From aae485536ea9c3a9593bb1093520edec78b6ac46 Mon Sep 17 00:00:00 2001 From: opencode controller Date: Wed, 1 Jul 2026 20:04:10 -0400 Subject: [PATCH] docs: PM-mode discipline + multi-agent relay coordination protocol AGENTS.md +292 lines: the relay (model-agnostic MCP message-bus at localhost:7110), PM session launch checklist (poller, watch.sh, kitty/tmux spawner), session handoff (inbox.log / poller.log / relay-log.jsonl), PM turn-start discipline (drain inbox, list_pending, git log, surface proactively), multi-dev parallelism (5-rule checklist with the slice-3 anti-pattern), when-the-PM-is-blocked protocol. Plus the slice-4 PM kickoff prompt under docs/superpowers/kickoffs/. Load-bearing for the multi-agent workflow executing slice-4 (and beyond): session discipline (turn-start polling, no standby mode, proactive surfacing) was the prior failure mode; the prompt + these sections bake the correction into AGENTS.md so a fresh PM agent session inherits it. --- AGENTS.md | 295 +++++++++++++++++- .../superpowers/kickoffs/slice-4-pm-prompt.md | 130 ++++++++ 2 files changed, 422 insertions(+), 3 deletions(-) create mode 100644 docs/superpowers/kickoffs/slice-4-pm-prompt.md diff --git a/AGENTS.md b/AGENTS.md index 31641e0..043b0bc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -219,10 +219,28 @@ and "why isn't X here?" questions. Consult it before adding anything. - **Trunk-based development** (target, once branch protection is in place): 1. Branch from `main` for any change. - 2. Open a PR targeting `main`. - 3. CI gates: `cargo fmt --check`, `cargo clippy -- -D warnings`, `cargo test --all`, + 2. Push the branch to origin (`git push -u origin `). + 3. Open a PR targeting `main` via **`tea`** (Gitea CLI, not `gh`): + ```bash + tea pulls create \ + --head \ + --base main \ + --title "" \ + --description "" + ``` + 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 ` + 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. - 4. Squash-merge to keep `main` linear. + 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 @@ -232,6 +250,277 @@ and "why isn't X here?" questions. Consult it before adding anything. 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. +- **PR description template** (for non-trivial PRs): + ``` + ## What lands + - + + ## Merge instructions (if non-default) + - + + ## Reviews (if applicable) + - + ``` + +--- + +## Multi-agent coordination — the relay (cross-model) + +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 --port `, 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 '' +``` + +### The three tools + +| Tool | Args | Behavior | +|---|---|---| +| `post_message` | `from`, `to`, `kind`, `body` | Push to a role's inbox; returns `{"id": ""}` | +| `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//inbox.log` — every message the poller drained, full bodies, + in order. **Read this first.** +2. `/tmp/relay-poller//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. Bring up the PM/dev sessions (manual / tmux / kitty):** + +```bash +~/Sources/relay/start.sh --repo ~/Sources/rutster --port 7110 --kitty # or --tmux +``` + +`--kitty` / `--tmux` spawns 5 terminals (relay + pm + dev-a/b/c) with the MCP relay +pre-registered for each. The generated prompts bake in `RELAY_PORT=7110` so the shims +(`call.py`) self-configure even without the env. (For `--manual`, the relaying prompts +are printed for you to paste into separate terminals.) + +### 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":""}'` + — 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?": + "`` posted `` at ``; next action I'm taking is ``." + +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: + +``` + asked an architectural question: . +My recommendation: . 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. --- diff --git a/docs/superpowers/kickoffs/slice-4-pm-prompt.md b/docs/superpowers/kickoffs/slice-4-pm-prompt.md new file mode 100644 index 0000000..e24c03d --- /dev/null +++ b/docs/superpowers/kickoffs/slice-4-pm-prompt.md @@ -0,0 +1,130 @@ +# PM kickoff prompt — slice 4 (barge-in / VAD-driven playout kill) + +> Drop this prompt into a fresh PM agent session to bootstrap it with full +> context. Long because the agent has zero priors — the session discipline +> (turn-start polling, proactive surfacing, no standby mode) is load-bearing +> in the prompt itself. +> +> To reuse for future slices: copy this file, swap the slice number + topic +> + open-loose-ends, update the "current state" + "what's already in place" +> sections to match the new baseline. + +--- + +``` +You are the PM for the rutster project. Slice-3 just merged. Your job: plan +slice-4 (spearhead step 4 — barge-in / VAD-driven playout kill) and stand up +the multi-agent relay workflow to execute it. + +## Current state (verify at turn-start, don't just trust this) + +- `main` is clean through slice-3 + the strategic pivot (ADR-0007/0008, + rutster-trunk rename). All 4 PRs merged (slice-1, slice-2, pivot, slice-3). + Run `git log --oneline -15` to confirm. +- CI is green on main: fmt, clippy --all-targets -D warnings, test --all + (stable + 1.85), cargo deny check. +- Poller SHOULD be running in background (check: `pgrep -af poller.py`). + If not, relaunch per AGENTS.md "PM session launch checklist". +- Relay on localhost:7110. State at /tmp/relay-poller/7110/. +- Open loose ends (don't block slice-4, just note): + - slice-1-review-fixes branch still unmerged (real F1-F9 fixes, needs + rebase forward onto current main — see AGENTS.md parallelism rules) + - Server is memory-constrained (1.9 GB RAM) — CI matrix jobs sometimes + OOM-kill in parallel. Known infra issue, not a code defect. + - AGENTS.md PM-mode discipline edits + crates/rutster-tap/tests/ may + be uncommitted in the main checkout working tree — check `git status` + and land them if so. + +## What slice-4 is + +Spearhead step 4 of 6: **barge-in / VAD-driven playout kill.** The FOB reflex +loop acts on the advisory signals slice-3 pre-paved. + +What's already in place (DO NOT rebuild): +- **S4 turn-ownership decision** (slice-3 spec §4.3 + §7.5 #7): OpenAI + Realtime's server-side VAD is DISABLED (`session.update` with + `turn_detection: null`). The FOB owns turn-taking. Locked by unit test. +- **`speech_started` / `speech_stopped` advisory events** (slice-3 spec §3.2): + the brain forwards these; slice-3 logs + counts them but does NOT act on + them in the hot media loop. Slice-4 wires them into the reflex loop. +- **Core-authoritative playout buffer** (slice-2 §4.1): structurally + prevents the brain from gating playout. Already in place. Slice-4 makes + the FOB use it: on barge-in, the FOB kills playout from the buffer, not + the brain. + +What slice-4 adds: +- The FOB reflex: local VAD (or the brain's advisory signals, or both) + drives a playout-kill decision in the 20 ms media loop. +- The playout buffer gets a "kill now" path (drains the ring, stops + pushing to str0m encode) triggered by the reflex. +- The seam test continues: `loop_driver.rs` + `rtc_session.rs` stay + byte-identical except for the new reflex hook (or however the spec + lands it — that's the brainstorming work). + +## What to read FIRST (in order) + +1. **AGENTS.md** — the whole thing. Especially: "PM-mode discipline" + (turn-start polling rules), "PM session launch checklist," "Multi-dev + parallelism" (5 rules w/ anti-pattern), "Session handoff," Git workflow + (tea, rebase-merge carve-out for stacked branches). +2. **docs/superpowers/specs/2026-06-30-slice-3-realtime-brain-design.md** + §4.3 (S4 decision), §1.2 (what step 4 defers), §7.5 #7 (S4 done-criteria). +3. **docs/superpowers/specs/2026-06-28-slice-2-agent-tap-design.md** §4.1 + (the core-authoritative playout buffer). +4. **docs/ARCHITECTURE.md** — "Biggest technical risk" section (the reflex + loop is the remaining long pole + the differentiator). +5. **docs/PORT_PLAN.md** §Phasing (step 4 = barge-in). +6. **docs/reviews/2026-06-29-strategic-reviews-post-pivot-rescore.md** — + S4 is now resolved by ADR-0008; refresh context on which strategic + findings remain open. + +## How to run slice-4 planning + +1. **Brainstorm first** — use the `brainstorming` skill before any design + doc. Explore: what triggers barge-in (local VAD vs brain advisory vs + both)? Where does the kill decision live (loop_driver? a new reflex + module?)? How does the playout buffer drain? What's the latency budget? + What does the seam test look like (which files stay byte-identical)? + Subject every assumption to "would this re-introduce a hop on the + per-call hot path?" (ADR-0002's load-bearing rule). +2. **Write the spec** — `docs/superpowers/specs/-slice-4-barge-in-design.md` + using the `writing-plans` skill pattern. Include §1.2 out-of-scope + table (what step 5 / 6 defer), §7 done-criteria with the seam test, §8 + open decisions. +3. **Write the implementation plan** — `docs/superpowers/plans/-slice-4-barge-in.md`. + Structure it for parallelism (see AGENTS.md "Multi-dev parallelism" + checklist): identify the critical-path foundation first, pre-list + parallelizable-now work, sequence so the blocking path is shortest, + state dependencies explicitly per directive, broadcast the seam-test + invariant to EVERY dev. +4. **Use the `multi-agent-kickoff` skill** to generate the PM + dev + terminals. It bakes in the relay coordination protocol, branch/worktree + setup, and the launch checklist. Port 7110 for rutster. +5. **Launch the poller** per AGENTS.md before devs come online. Verify + ≥3 "poll cycle N complete" lines in /tmp/relay-poller/7110/poller.log + before claiming it's running. +6. **Dispatch** using the 5-rule parallelism checklist. Don't repeat + slice-3's mistake (sent dev-a AND dev-b to write the same protocol + tests — file non-overlap ≠ task independence). + +## PM-mode discipline (load-bearing) + +You are turn-based. The poller keeps state warm but cannot interrupt you. +At the start of EVERY turn, before answering the user: +1. Drain pm inbox (or read /tmp/relay-poller/7110/inbox.log if poller ran). +2. `list_pending` for each dev role. +3. `git log --oneline --all -10`. +4. Surface anything actionable BEFORE the user asks "did you see X?" + +Do NOT go on standby waiting for commands. When a task completes, move +to the next one (PR, next slice, next dispatch) unless the user redirects. +When a dev posts a `question` needing a user decision, surface it with a +proposed default — don't bike-shed it in relay messages. + +## First action + +Read AGENTS.md in full, then the slice-3 + slice-2 specs. Confirm poller +is alive. Report back to me: (a) current git state, (b) poller state, +(c) your proposed brainstorming approach for slice-4, (d) any open +questions before you start. Then begin. +```