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>
31 KiB
Agent Guide: rutster
Rutster is the open-source engine for building the AI-era contact center — self-hostable,
AI-native, memory-safe Rust. A framework/engine (not a turnkey product); a spiritual successor to
Asterisk's place in the world, not its protocols or architecture. See README.md
for the full vision and docs/superpowers/specs/2026-06-28-slice-1-webrtc-loopback-design.md
for the active build target.
This file orients any agent (human, AI, hybrid) working in the repo.
Project structure (current + planned)
rutster/
├── README.md # vision, persona, wedge, capability ladder
├── AGENTS.md # this file
├── LEARNING.md # (planned) index of "to learn concept X, read file Y"
├── Cargo.toml # (planned) [workspace] manifest
├── deny.toml # (planned) cargo-deny config
├── rust-toolchain.toml # (planned) pinned stable
├── crates/ # (planned) workspace members, ADR-0002-fused-vertical shape
│ ├── rutster/ # binary: axum signaling server + media driver + static page
│ ├── rutster-media/ # str0m WebRTC + Opus<->PCM codec boundary
│ ├── rutster-call-model/ # the Channel/Leg object embryo
│ ├── rutster-trunk/ # stub until spearhead step 5
│ ├── rutster-tap/ # stub until spearhead step 2
│ └── rutster-spend/ # stub until spearhead step 6
├── fuzz/ # (planned) placeholder cargo-fuzz harness dir
├── docs/
│ ├── ARCHITECTURE.md # fused per-call vertical + composable platform
│ ├── PORT_PLAN.md # capability checklist + thin-slice phasing
│ ├── adr/ # Architecture Decision Records (read before design work)
│ └── superpowers/specs/ # design specs (brainstorming → plan → implementation)
└── .github/workflows/ci.yml # (planned) fmt, clippy -D warnings, test --all, cargo deny check
Items marked (planned) are not yet on disk; they land with slice-1 implementation. Until then the repo is docs-only.
Build / lint / test commands
Rust (when the workspace exists):
cargo fmt --check # formatting check (CI gate)
cargo clippy -- -D warnings # lints (CI gate; warnings = failures)
cargo test --all # all unit + integration tests across the workspace
cargo deny check # licenses, advisories, bans, sources (CI gate)
cargo doc --no-deps --open # render the API docs (slice 1 heavily commented for learners)
Per-crate iteration:
cargo test -p rutster-media # one crate's tests
cargo test -p rutster-media -- --nocapture # see println! output
cargo run -p rutster # run the binary (axum on 0.0.0.0:8080)
RUST_LOG=rutster=debug cargo run # verbose tracing
Docs-only iterations (current state):
# validate markdown links + structure
ls docs/adr/ docs/superpowers/specs/
There is no Python, no Node, no Docker in the dev loop for slice 1. The batteries-included
compose up is a later-rung concern (lands with Valkey + trunk).
Code style (Rust)
Formatting & linting
cargo fmtis the single source of truth for whitespace/indentation. Don't hand-format.clippy -D warningsis the lint bar. CI fails on any warning. Fix the code, don't suppress with#[allow]unless the rationale is documented inline.
Naming
snake_casefor functions, methods, variables, modules, crates.PascalCasefor types (struct, enum, trait).UPPER_SNAKE_CASEfor constants.newtypewrappers over primitives for type-safety (e.g.ChannelId(Uuid), not bareUuid) — seerutster-call-model. The pattern prevents mixing up aChannelIdwith aSessionIdat the type system level.
Error handling
- Cold path (signaling, setup, request handlers):
thiserror-derived error enums,?propagation, converted to HTTP status codes at the axum boundary. - Hot path (the 20 ms media loop): never
?-propagate. Match-and-continue. A dropped packet must not terminate the peer. Policy: "drop + observe (log + counter), don't crash." This is the posture the eventual fuzz harness will test against. - Never
unwrap()/expect()outside tests or const-initialization contexts. Use?or explicit match.
Async & concurrency
- tokio for the control plane and for slice-1 media polling (acknowledged deviation from ARCHITECTURE.md, which mandates dedicated timing threads — see slice-1 spec §3.4).
Arc<Mutex<T>>for short-held shared state; preferArc<RwLock<T>>only when reads dominate writes. Comment the choice inline (it's a learner-facing item).- Sans-IO design where the slice-1 spec calls for it (str0m
Livepolling). The code comments explain why: a sans-IO component is one that takes input via method calls and produces output via return values, never touching IO directly — making it fully testable without a network.
Documentation comments (learner-facing — important)
This project overrides the default "no comments" convention. The user is learning Rust from this codebase. Slice 1 carries thorough educational comments:
//!module docs at the top of everylib.rs/main.rs/ sub-module: what the module does, why it exists in the architecture (cross-ref the relevant ADR / PORT_PLAN row), key types.///item docs on every public struct / enum / fn / trait: purpose + short example where non-obvious. Must render correctly incargo doc.//inline comments on the mechanism, not the what — whyPin<Box<dyn Future>>instead ofasync fn, whyArc<Mutex<...>>vsArc<RwLock<...>>, whatPhantomDatais doing, why anenumwas chosen over astructwith akindfield. Aim: a Rust learner reads the comment and learns a specific Rust concept they wouldn't have inferred from the code alone.- str0m-specifics flagged: every str0m interaction gets a comment explaining what str0m is doing and why we drive it that way.
- Ownership / borrowing decisions called out the first time each non-obvious pattern appears.
This verbosity is a deliberate trade-off: more tokens to skim now, compound educational value later. Once a pattern is established and the reader has learned it, later slices can be sparser on the well-trodden patterns.
Terminology policy (inclusive language)
Avoid authoritarian / exclusionary terms in our own code, prose, identifiers, and endpoint names. Use equally-descriptive alternatives:
| Avoid | Use instead |
|---|---|
| police / policing (the verb) | enforce / gate / guard |
| master / slave | primary / replica, leader / follower, controller / worker |
| blacklist / whitelist | denylist / allowlist, blocklist / safelist |
| officer | operator / handler / controller |
| censor | suppress / filter |
Exception: protocol-convention names are kept verbatim when they come from upstream
specs or libraries we depend on — replacing them would hurt the educational mapping to
upstream documentation. Concretely, ICE (Interactive Connectivity Establishment,
RFC 8445) stays: it's the protocol name in str0m::ice, RTCIceCandidate, and the cargo
crate ecosystem. Our prose around it can say "NAT traversal" / "connectivity candidates"
where that reads better, but identifiers and protocol-level references keep ICE.
Same logic for any future RFC-defined acronym.
Architecture pre-reading (required before design work)
Before proposing changes to the architecture, read in this order:
README.md— north star, persona, wedge, capability ladder (10 min).docs/ARCHITECTURE.md— fused per-call vertical + composable platform, the agent tap as central interface (15 min).docs/PORT_PLAN.md— capability checklist + disposition per subsystem + thin-slice phasing (20 min).docs/adr/— every ADR. Load-bearing decisions, not optional reading:- ADR-0002 — north star + fused vertical
- ADR-0003 —
Rust-native trunk SIPsuperseded by ADR-0007 - ADR-0004 — GPL-3.0-or-later
- ADR-0005 — Valkey as bus + state store
- ADR-0006 — WebRTC-first ingress
- ADR-0007 — rent the trunk transport; no first-party SIP stack
- ADR-0008 — the FOB / green-zone build-vs-reuse doctrine
docs/superpowers/specs/— design specs in flight. Read the latest one to know what's currently being built and what's explicitly deferred.- 2026-06-26 vision-revision — the pressure-test that produced the current architecture.
- 2026-06-28 slice-1 WebRTC loopback — the active build target.
The slice-1 spec's §1.2 out-of-scope table is the single source of truth for "is X done?" and "why isn't X here?" questions. Consult it before adding anything.
Key decisions to respect
- The FOB / green-zone doctrine (ADR-0008) — the build-vs-reuse rule. Build it in the memory-safe Rust FOB only if it's hot-path, security-constitutive, or differentiating; otherwise reuse trusted, actively-maintained OSS in the green zone, at arm's length (its own process / container / trust domain). 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.
- License:
GPL-3.0-or-lateron every crate manifest (ADR-0004). Strong copyleft in the Asterisk lineage. Don't introduce deps that conflict (cargo deny check licensesenforces). - WebRTC stack:
str0m(sans-IO). Notwebrtc-rs. Chosen because the sans-IO design maps directly onto ARCHITECTURE.md's "dedicated timing threads, not the shared tokio pool." - Workspace shape: full ADR-0002-fused-vertical layout. Stub crates are explicitly
permissibly empty (
lib.rswith doc comment + acrate_compiles()test). They lock boundaries, not anticipate code. - Agent tap posture: core-as-client, brain-as-server. No inbound tap port on the core. Tap = egress; ingress = inbound (WebRTC) — opposite security postures, never unified (ADR-0006). Don't blur this line.
- In-boundary spend/abuse control is constitutive of the wedge (ADR-0002). Pulling it out into a service re-introduces the 3-vendor structural hole. Don't externalize it.
- Fused per-call vertical — the control↔media gRPC hop on the per-call hot path is removed by design (ADR-0002). Don't re-introduce it.
- No WASM in the core story (ADR-0002 demoted it). The agent tap is the extension point for in-call logic.
Git workflow
- Trunk-based development (target, once branch protection is in place):
- Branch from
mainfor any change. - Push the branch to origin (
git push -u origin <branch>). - Open a PR targeting
mainviatea(Gitea CLI, notgh):Thetea pulls create \ --head <branch> \ --base main \ --title "<imperative subject ≤72 chars>" \ --description "<body — what, why, merge instructions, reviews>"tealogin isalee(default), againsthttps://git.adlee.work. Verify withtea login listif auth fails.tea pulls listshows open PRs;tea pulls merge <index>merges from CLI (honors the merge strategy passed via--style). - CI gates:
cargo fmt --check,cargo clippy -- -D warnings,cargo test --all,cargo deny check. All must pass before merge. - Merge strategy — default squash, carve-out for stacked branches:
- Default: squash-merge to keep
mainlinear. One commit per PR, clean history. - Rebase-merge carve-out: when a PR's commit SHAs are carried by downstream branches (the "stacked branches" topology — pivot/strategic-doc PRs that depend on each other), use rebase-merge instead. This preserves the original commit SHAs so downstream branches see the pivot as "already in main" with zero rebase pain. Squash would orphan the originals and force conflict-prone rebases across every dependent branch. State "merge via rebase-merge, not squash" explicitly in the PR description when this applies.
- Default: squash-merge to keep
- Branch from
- Never push directly to
main. Branch protection (planned) will enforce; until then, self-discipline. - Commit messages: imperative mood, subject ≤ 72 chars, body wraps at 72, blank line
between subject and body. Reference ADRs / specs by number when relevant. Match the style
of recent commits (
git log --oneline -10). - Atomic commits: one logical change per commit. Doc ratifications, code, and tests each land as separate commits when practical. Don't bundle unrelated work.
- Never commit secrets. The
.gitignorealready covers.env*,*.pem,*.key. If a new secret pattern appears, extend.gitignorein the same commit. - Developer Certificate of Origin (DCO) — REQUIRED on every commit. Every commit
MUST be signed off (
git commit -sorgit commit --signoff) to attest the author has the right to contribute the code under the project's license (GPL-3.0-or-later, ADR-0004). This preserves the copyright-holder's option to dual-license later — without a DCO/CLA instrument, every external contribution erodes that option. SeeDCO.mdfor the full text. The signoff line (Signed-off-by: Name <email>) is the canonical attestation; CI (planned) will reject unsigned commits. Agents committing on behalf of a human MUST sign off with the human's name + email, not their own identity. - PR description template (for non-trivial PRs):
## What lands - <bullet per logical change> ## Merge instructions (if non-default) - <e.g. "rebase-merge, not squash — see AGENTS.md Git workflow"> ## Reviews (if applicable) - <companion review docs / ADRs / specs>
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. One server, many terminals: each
session posts and drains messages by role instead of the user copy-pasting blocks
between windows.
The relay is model-agnostic. Nothing about it is Claude-specific — it speaks plain MCP / JSON-RPC over HTTP. A GLM-backed agent and a Claude agent share the same bus and exchange messages transparently, as long as both point at the same running relay and use the same role/kind vocabulary below.
- Currently running on
localhost:7110. The port is set byRELAY_PORT(default7331if unset). The server binds127.0.0.1, so all agents must be on this host (a GLM agent in a separate container/VM can't reach it over localhost). - Roles are slots, not models. The relay knows
pm, dev-a, dev-b, dev-c, dev-d, dev-e, dev-f— that's it. A GLM agent claims a free role (saydev-c) exactly like a Claude agent claimsdev-a; the relay neither knows nor cares which model drives a role. - Start the relay before opening sessions if you want MCP auto-registration. The shell shim (below) works at any time.
Port assignments (per project)
Each project gets its own relay port so multiple coordinated lifts can run side by side
without colliding. Set RELAY_PORT to the project's port when launching the relay and in
every agent/shim that talks to it.
| Port | Project | Notes |
|---|---|---|
7110 |
~/Sources/rutster |
this repo |
7331 |
~/Sources/relicario |
also the relay's built-in default |
Adding a project? Pick an unused port, launch with
~/Sources/relay/start.sh --repo <path> --port <n>, and add the row here.
Two ways for an agent to connect
1. MCP-native — for any harness that speaks MCP over SSE (this includes Claude Code
pointed at a GLM model). Register the relay as an SSE server, then call post_message /
read_messages / list_pending as native tools. Config shape:
// .mcp.json (or your harness's MCP config)
{ "mcpServers": { "relay": { "type": "sse", "url": "http://localhost:7110/sse" } } }
For Claude Code specifically: claude mcp add --transport sse relay http://localhost:7110/sse.
2. Shell shim — for any agent that can run a shell command (model- and harness-agnostic;
the fallback when MCP isn't registered). The shims read RELAY_PORT, so export it once:
export RELAY_PORT=7110 # match the running relay
python3 ~/Sources/relay/call.py list_pending '{"for":"dev-c"}'
python3 ~/Sources/relay/call.py read_messages '{"for":"dev-c"}' # drains the inbox
python3 ~/Sources/relay/call.py post_message \
'{"from":"dev-c","to":"pm","kind":"status","body":"Slice-1 media codec boundary DONE. Tests green."}'
# call.ts is the TypeScript equivalent: npx tsx ~/Sources/relay/call.ts <tool> '<json>'
The three tools
| Tool | Args | Behavior |
|---|---|---|
post_message |
from, to, kind, body |
Push to a role's inbox; returns {"id": "<uuid>"} |
read_messages |
for |
Drains the inbox (consume-once), oldest first |
list_pending |
for |
Count + kinds without consuming |
kind is one of status (a dev's STATUS UPDATE / PM rollup), directive (PM → dev:
PROCEED / HOLD / RESCOPE), question (dev → PM), or free (anything else). Keep body
single-line (periods between sentences, -- for stronger breaks) — some inbox monitors
use strict JSON parsers that choke on embedded newlines.
A GLM dev-c posting a status and a Claude pm reading it is the same roundtrip in both
directions — the bus is symmetric. See ~/Sources/relay/README.md
for roles, ports, and the pm convenience wrapper.
Multi-dev parallelism — dispatching work without serial bottlenecks
When the PM dispatches a slice across N devs, the goal is N devs actively working, not N-1 blocked on one. File-level non-overlap (no merge conflicts) is necessary but NOT sufficient — task dependencies, not file ownership, are what stall a dev. Apply this checklist to every multi-dev dispatch:
- Identify the critical-path foundation first. Anything consumed by other tasks (a new trait + impl others extend, an mpsc channel others wire into, a mock others depend on) belongs to ONE dev's "foundation" sub-task that lands FIRST. Don't parallelize across a dependency; parallelize the tail AFTER the foundation lands.
- Pre-list "parallelizable-now" work explicitly, per slice, up front.
Before dispatching, scan the spec's test section (often §7.4) and §8
done-criteria for: unit tests against already-existing types, golden JSON
fixtures, harness scaffolding, docs,
examples/, LEARNING.md pointers, README e2e plans, behavior-preserving refactors. These are the "filler" a blocked dev picks up instead of stalling. List them in the dispatch so a blocked dev knows the fallback without asking. - Sequence so the blocking path is the shortest one. If dev-a's foundation takes 30 min and dev-b's dependent work takes 20 min, the parallelizable tail needs to be >50 min of work or you'll have idle devs. Either bundle more into the foundation (so less depends on it) or split the dependent work across more devs.
- State dependencies explicitly in each directive. Every directive names: (a) files the dev owns, (b) files that are off-limits (seam-test invariants), (c) blockers — which dev's status unblocks this dev, and what to do in the meantime (the "filler" from rule 2).
- The seam test is sacred. When a slice has a "these files stay
byte-identical to baseline" gate (e.g. slice-2/3's
loop_driver.rs+rtc_session.rs), tell EVERY dev not to touch them. Don't rely on one dev's directive; broadcast the constraint.
Anti-pattern (real, from slice-3): split by file (no conflict) but one dev's task consumed another's unwritten outputs (mpc channels + mock). File non-overlap still produced a serial bottleneck because the task dependency wasn't surfaced. Fix: rule 1 + 2 + 4 together.
Background poller (poller.py) — for PM sessions that monitor the relay over time
A PM session that drains the inbox only when prompted will miss time-sensitive questions
from devs and let directives stall. Launch the relay's poller.py as a background
process at the start of every PM session (one per relay port — for rutster that's 7110):
setsid env RELAY_PORT=7110 python3 ~/Sources/relay/poller.py \
>> /tmp/relay-poller/7110/poller.log 2>&1 < /dev/null & disown
What it does (every 20s by default): lists/drains the PM inbox (logging full bodies to
/tmp/relay-poller/7110/inbox.log), lists pending for each dev role, and posts a
question-kind nudge if a directive has sat undrained >120s. State files persist across
PM sessions — read inbox.log to catch up on what the poller drained while you were away,
and poller.log to confirm the poller is actually cycling.
Verification discipline (load-bearing — this was a real failure mode): after launching, wait ≥60s, then confirm the log shows at least 3 "poll cycle N complete" lines before claiming the poller is running. A "poller start" line + a live process is NOT sufficient — the SSE client can launch then silently fail on the first poll. The cycle entries are the proof of life. If you only see the start line, kill + relaunch + re-verify.
Do NOT use call.py in a loop. Each call.py invocation opens a fresh SSE
connection + spawns a daemon thread that doesn't clean up. Over hundreds of polls
this piles up connections/threads and eventually wedges. poller.py opens ONE
persistent SSE connection and reuses it — that's the whole point of the distinction.
For dev sessions: dev agents don't need the poller. They should drain their own inbox at the start of every turn (or poll every ~20-30s if the harness supports background polling). The poller exists to keep PM-side state warm and catch devs who go quiet — devs are the consumers of directives, not the pollers.
Session handoff — what to read when resuming a PM session
When a PM session resumes (new terminal, cleared context, etc.), the relay's in-memory queues may have been drained by the poller in the previous session. Reconstruct state from, in order:
/tmp/relay-poller/<port>/inbox.log— every message the poller drained, full bodies, in order. Read this first./tmp/relay-poller/<port>/poller.log— poll-cycle entries, nudges posted, errors. Skim for any nudge you sent that the dev never responded to.~/Sources/relay/relay-log.jsonl— the relay's own append-only archive, the durable full-body record of every posted message (the in-memory queues are consume-once and vanish on relay restart; this file is the source of truth across restarts). Use this if the poller state dir was wiped.git log --oneline --all -20— what actually landed; reconcile against the statuses in inbox.log.
The PM session's first action on resume is reading these — not posting directives. You cannot direct devs whose last status you don't know.
PM session launch checklist — reproducible setup for new sessions
When you (the user) open a fresh set of kitty/tmux terminals for a new PM + N-dev lift, run this checklist so polling "just works" without manual relay between you and the PM agent:
1. Start the relay before opening agent sessions. The PM and dev agents auto-register their MCP tools against the relay on startup — but only if the relay is already running. For rutster:
~/Sources/relay/start.sh --repo ~/Sources/rutster --port 7110
(This terminal becomes the relay log. Leave it open, or background it.)
2. Launch the poller in the background (PM-side inbox monitoring):
setsid env RELAY_PORT=7110 python3 ~/Sources/relay/poller.py \
>> /tmp/relay-poller/7110/poller.log 2>&1 < /dev/null & disown
The poller drains the pm inbox every 20s and logs full bodies to
/tmp/relay-poller/7110/inbox.log. It also nudges any dev whose directive inbox stays
undrained >120s. State files persist across PM sessions, so closing the PM terminal
doesn't lose history.
3. Open a side kitty window for live inbox + poller tail:
# In a kitty tab alongside the PM chat:
RELAY_PORT=7110 ~/Sources/relay/watch.sh
This live-tails both inbox.log (dev → pm messages) and poller.log (poll cycle
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. Launch the PM session (single terminal — the PM auto-spawns the devs):
~/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
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)
The poller and watch.sh capture state, but the PM agent is still turn-based — a background process cannot interrupt the PM mid-conversation. The user's prompt is the only thing that spins the PM up. Therefore, at the start of every turn, before answering the user's question or acting on it, the PM agent MUST:
- Drain its own inbox via
python3 ~/Sources/relay/call.py read_messages '{"for":"pm"}'(or readinbox.logif the poller has been running). - Poll pending for each dev role:
python3 ~/Sources/relay/call.py list_pending '{"for":"<role>"}'— catches directives that haven't been picked up. - Check
git log --oneline --all -10for commits that landed since last seen. - Surface anything actionable to the user BEFORE they have to ask "did you see X?":
"
<dev>posted<status>at<ts>; next action I'm taking is<action>."
If a dev posted a question kind that needs a user decision (architectural forks,
build-vs-reuse calls, etc.), surface it explicitly with a proposed default and ask for
confirmation — don't go silent on it and don't proceed without the user's call.
Anti-pattern (real, from slice-3): the poller was running and draining state correctly, but the PM only read it when the user asked "did you see X?" — wasting the poller's value. The turn-start discipline above is what makes the poller actually useful.
When the PM is blocked on a user decision
Devs posting question-kind messages often need a user-only decision (architecture
forks, build-vs-reuse, licensing posture, etc.). The PM should NOT bike-shed these in
relay messages — surface them to the user immediately and propose a default:
<dev-X> asked an architectural question: <one-line summary>.
My recommendation: <default>. Confirm or redirect?
Devs don't wait for the PM to bike-shed; they wait for the user. The PM's job is to
telescope the decision needed to one line, propose a sound default, and let the user
confirm. If the user is away, the PM can post a status to the dev saying "blocked on
user decision re: , you'll get a directive when they confirm" so the dev can pick up
parallel work or stand down.
Slice-1 boundaries — what NOT to add (yet)
These are explicitly deferred per the slice-1 spec's out-of-scope table. Adding them NOW would break the sequencing that the spearhead depends on:
- ❌ Dedicated timing thread for the media loop (deferred to step 4, barge-in)
- ❌ TLS on the HTTP signaling surface (deferred to step 5, rented-transport PSTN)
- ❌ Authn / authz / multi-tenancy on
/v1/sessions(deferred to step 6, spend cap) - ❌ Trickle ICE (deferred until NATs demand it)
- ❌ The tap itself (deferred to step 2 — slice 1 only pre-paves the seam)
- ❌ The brain / STT / LLM / TTS (deferred to step 3)
- ❌ Barge-in / VAD-driven playout kill (deferred to step 4)
- ❌ PSTN via rented transport / CPaaS media-leg ingress (deferred to step 5; no first-party SIP — ADR-0007)
- ❌ Spend cap / abuse gate (deferred to step 6)
- ❌ Browser-based automated e2e tests / Selenium / Playwright (deferred post-slice-1)
- ❌ Docker / compose (deferred to a later rung)
- ❌ Event bus / Valkey / CDR emission (deferred to step 5)
- ❌ Transfer / park / pickup / barge features (deferred to escalation rung 2)
If an agent proposes adding any of these in slice 1, the right answer is "no, see the slice-1 spec §1.2."
What's next
The active task is implementing slice 1 per
docs/superpowers/specs/2026-06-28-slice-1-webrtc-loopback-design.md.
The brainstorming phase is complete; the next step is the implementation plan (via the
writing-plans skill), then TDD execution.