docs: PM-mode discipline + multi-agent relay coordination protocol
All checks were successful
CI / fmt (pull_request) Successful in 1m44s
CI / clippy (pull_request) Successful in 2m31s
CI / test (1.85) (pull_request) Successful in 5m54s
CI / test (stable) (pull_request) Successful in 6m20s
CI / deny (pull_request) Successful in 2m2s

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.
This commit is contained in:
opencode controller
2026-07-01 20:04:10 -04:00
parent c30a45232d
commit aae485536e
2 changed files with 422 additions and 3 deletions

295
AGENTS.md
View File

@@ -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): - **Trunk-based development** (target, once branch protection is in place):
1. Branch from `main` for any change. 1. Branch from `main` for any change.
2. Open a PR targeting `main`. 2. Push the branch to origin (`git push -u origin <branch>`).
3. CI gates: `cargo fmt --check`, `cargo clippy -- -D warnings`, `cargo test --all`, 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. `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, - **Never push directly to `main`.** Branch protection (planned) will enforce; until then,
self-discipline. self-discipline.
- **Commit messages:** imperative mood, subject ≤ 72 chars, body wraps at 72, blank line - **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. each land as separate commits when practical. Don't bundle unrelated work.
- **Never commit secrets.** The `.gitignore` already covers `.env*`, `*.pem`, `*.key`. If - **Never commit secrets.** The `.gitignore` already covers `.env*`, `*.pem`, `*.key`. If
a new secret pattern appears, extend `.gitignore` in the same commit. a new secret pattern appears, extend `.gitignore` in the same commit.
- **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)
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. 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":"<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.
--- ---

View File

@@ -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/<date>-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/<date>-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.
```