All checks were successful
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>
580 lines
31 KiB
Markdown
580 lines
31 KiB
Markdown
# 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`](README.md)
|
|
for the full vision and [`docs/superpowers/specs/2026-06-28-slice-1-webrtc-loopback-design.md`](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):**
|
|
|
|
```bash
|
|
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:**
|
|
|
|
```bash
|
|
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):**
|
|
|
|
```bash
|
|
# 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 fmt` is the single source of truth for whitespace/indentation. Don't hand-format.
|
|
- `clippy -D warnings` is the lint bar. CI fails on any warning. Fix the code, don't suppress
|
|
with `#[allow]` unless the rationale is documented inline.
|
|
|
|
### Naming
|
|
|
|
- `snake_case` for functions, methods, variables, modules, crates.
|
|
- `PascalCase` for types (struct, enum, trait).
|
|
- `UPPER_SNAKE_CASE` for constants.
|
|
- `newtype` wrappers over primitives for type-safety (e.g. `ChannelId(Uuid)`, not bare `Uuid`) —
|
|
see `rutster-call-model`. The pattern prevents mixing up a `ChannelId` with a `SessionId` at
|
|
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; prefer `Arc<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 `Live` polling). 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 every `lib.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 in `cargo doc`.
|
|
- `//` inline comments on the *mechanism*, not the what — why `Pin<Box<dyn Future>>` instead of
|
|
`async fn`, why `Arc<Mutex<...>>` vs `Arc<RwLock<...>>`, what `PhantomData` is doing, why an
|
|
`enum` was chosen over a `struct` with a `kind` field. 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:
|
|
|
|
1. [`README.md`](README.md) — north star, persona, wedge, capability ladder (10 min).
|
|
2. [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) — fused per-call vertical + composable platform,
|
|
the agent tap as central interface (15 min).
|
|
3. [`docs/PORT_PLAN.md`](docs/PORT_PLAN.md) — capability checklist + disposition per subsystem +
|
|
thin-slice phasing (20 min).
|
|
4. [`docs/adr/`](docs/adr/) — every ADR. Load-bearing decisions, not optional reading:
|
|
- [ADR-0002](docs/adr/0002-north-star-and-fused-core.md) — north star + fused vertical
|
|
- [ADR-0003](docs/adr/0003-sip-rust-native-trunk.md) — ~~Rust-native trunk SIP~~ **superseded by [ADR-0007](docs/adr/0007-trunk-rented-transport.md)**
|
|
- [ADR-0004](docs/adr/0004-license.md) — GPL-3.0-or-later
|
|
- [ADR-0005](docs/adr/0005-event-bus.md) — Valkey as bus + state store
|
|
- [ADR-0006](docs/adr/0006-ingress-posture.md) — WebRTC-first ingress
|
|
- [ADR-0007](docs/adr/0007-trunk-rented-transport.md) — rent the trunk transport; no first-party SIP stack
|
|
- [ADR-0008](docs/adr/0008-fob-and-green-zone.md) — the FOB / green-zone build-vs-reuse doctrine
|
|
5. [`docs/superpowers/specs/`](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](docs/superpowers/specs/2026-06-26-vision-revision-design.md) —
|
|
the pressure-test that produced the current architecture.
|
|
- [2026-06-28 slice-1 WebRTC loopback](docs/superpowers/specs/2026-06-28-slice-1-webrtc-loopback-design.md) —
|
|
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-later` on every crate manifest (ADR-0004). Strong copyleft in the
|
|
Asterisk lineage. Don't introduce deps that conflict (`cargo deny check licenses` enforces).
|
|
- **WebRTC stack:** `str0m` (sans-IO). Not `webrtc-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.rs` with doc comment + a `crate_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):
|
|
1. Branch from `main` for any change.
|
|
2. Push the branch to origin (`git push -u origin <branch>`).
|
|
3. Open a PR targeting `main` via **`tea`** (Gitea CLI, not `gh`):
|
|
```bash
|
|
tea pulls create \
|
|
--head <branch> \
|
|
--base main \
|
|
--title "<imperative subject ≤72 chars>" \
|
|
--description "<body — what, why, merge instructions, reviews>"
|
|
```
|
|
The `tea` login is `alee` (default), against `https://git.adlee.work`. Verify with
|
|
`tea login list` if auth fails. `tea pulls list` shows open PRs; `tea pulls merge <index>`
|
|
merges from CLI (honors the merge strategy passed via `--style`).
|
|
4. CI gates: `cargo fmt --check`, `cargo clippy -- -D warnings`, `cargo test --all`,
|
|
`cargo deny check`. All must pass before merge.
|
|
5. **Merge strategy — default squash, carve-out for stacked branches:**
|
|
- **Default:** squash-merge to keep `main` linear. 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.
|
|
- **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 `.gitignore` already covers `.env*`, `*.pem`, `*.key`. If
|
|
a new secret pattern appears, extend `.gitignore` in the same commit.
|
|
- **Developer Certificate of Origin (DCO) — REQUIRED on every commit.** Every commit
|
|
MUST be signed off (`git commit -s` or `git 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. See
|
|
[`DCO.md`](DCO.md) for 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`](file:///home/alee/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 by `RELAY_PORT` (default
|
|
`7331` if unset). The server binds `127.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 (say `dev-c`) exactly like a
|
|
Claude agent claims `dev-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`](file:///home/alee/Sources/rutster) | **this repo** |
|
|
| `7331` | [`~/Sources/relicario`](file:///home/alee/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:
|
|
|
|
```jsonc
|
|
// .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:
|
|
|
|
```bash
|
|
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`](file:///home/alee/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:
|
|
|
|
1. **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.
|
|
2. **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.
|
|
3. **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.
|
|
4. **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).
|
|
5. **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 `question`s
|
|
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):
|
|
|
|
```bash
|
|
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:
|
|
|
|
1. `/tmp/relay-poller/<port>/inbox.log` — every message the poller drained, full bodies,
|
|
in order. **Read this first.**
|
|
2. `/tmp/relay-poller/<port>/poller.log` — poll-cycle entries, nudges posted, errors.
|
|
Skim for any nudge you sent that the dev never responded to.
|
|
3. `~/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.
|
|
4. `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:
|
|
|
|
```bash
|
|
~/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):**
|
|
|
|
```bash
|
|
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:**
|
|
|
|
```bash
|
|
# 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):**
|
|
|
|
```bash
|
|
~/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:
|
|
|
|
1. Drain its own inbox via `python3 ~/Sources/relay/call.py read_messages '{"for":"pm"}'`
|
|
(or read `inbox.log` if the poller has been running).
|
|
2. Poll pending for each dev role: `python3 ~/Sources/relay/call.py list_pending '{"for":"<role>"}'`
|
|
— catches directives that haven't been picked up.
|
|
3. Check `git log --oneline --all -10` for commits that landed since last seen.
|
|
4. 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: <X>, 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`](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.
|