diff --git a/docs/superpowers/specs/2026-05-02-relay-server-design.md b/docs/superpowers/specs/2026-05-02-relay-server-design.md new file mode 100644 index 0000000..b38a425 --- /dev/null +++ b/docs/superpowers/specs/2026-05-02-relay-server-design.md @@ -0,0 +1,200 @@ +# Relay Server Design + +**Date:** 2026-05-02 +**Status:** Approved +**Scope:** Dev tooling — not shipped in any product artifact + +--- + +## Problem + +Multi-agent development lifts (PM + Dev-A + Dev-B in parallel Claude Code sessions) require passing status updates, questions, and directives between terminals. Today the user manually copies and pastes every message block. This is error-prone, breaks flow, and scales poorly as lift complexity grows. + +## Goal + +A lightweight MCP server running on localhost that gives all three Claude Code sessions native tools to post and read messages. The user stops being the message bus. + +--- + +## Repository layout + +``` +tools/relay/ +├── package.json # private, not published; single dep: @modelcontextprotocol/sdk +├── tsconfig.json +├── server.ts # MCP SSE server entry point (~150 lines) +├── queue.ts # in-memory queue logic (~50 lines) +├── queue.test.ts # Node built-in test runner +└── start.sh # launcher script +``` + +Added to root `.gitignore`: `tools/relay/node_modules/`, `tools/relay/dist/`. + +--- + +## Tech stack + +- **Runtime:** Node.js (v25, already installed at `/usr/bin/node`) +- **Package manager:** npm (bun has known compat gaps with the MCP SDK's SSE transport) +- **Dependencies:** `@modelcontextprotocol/sdk`, `tsx` (devDependency, runs TypeScript directly — no compile step) +- **Transport:** SSE (`SSEServerTransport` from the SDK handles the HTTP layer — no Express or Hono needed) +- **Port:** `7331` (hardcoded; easy to remember, unlikely to collide) + +--- + +## Queue model + +Three named inboxes: `pm`, `dev-a`, `dev-b`. Each is a FIFO array in memory. + +Message shape: + +```ts +interface RelayMessage { + id: string; // uuid v4 + from: string; // sender role name + to: string; // recipient role name + kind: "status" | "question" | "directive" | "free"; + body: string; // freeform, typically the existing markdown block format + ts: string; // ISO 8601 +} +``` + +`kind` maps to the existing coordination protocol: + +| kind | existing block | +|-------------|-----------------------------| +| `status` | `## STATUS UPDATE — DEV-*` | +| `question` | `## QUESTION TO PM — DEV-*` | +| `directive` | `## DIRECTIVE TO DEV-*` | +| `free` | ad-hoc / unstructured | + +Messages are **consume-once**: `read_messages` drains the inbox. There is no persistence — if the server restarts mid-lift, in-flight messages are lost. Acceptable for a dev tool; agents re-send on reconnect. + +--- + +## MCP tool surface + +All three tools are exposed to every connected session. + +### `post_message` + +``` +post_message(from: "pm"|"dev-a"|"dev-b", to: "pm"|"dev-a"|"dev-b", kind: "status"|"question"|"directive"|"free", body: string) → { id: string } +``` + +Pushes one message onto the target's inbox. Returns the assigned message id. Errors if `to` or `from` is not a known role. Agents declare their own identity via `from` — the kickoff prompt tells each agent its role name. + +### `read_messages` + +``` +read_messages(for: "pm"|"dev-a"|"dev-b") → RelayMessage[] +``` + +Pops and returns all pending messages for that recipient, in FIFO order. After this call the inbox is empty. + +### `list_pending` + +``` +list_pending(for: "pm"|"dev-a"|"dev-b") → { count: number, kinds: string[] } +``` + +Returns count and kind breakdown of pending messages without consuming them. Lets an agent cheaply check "do I have anything to act on?" before committing to a `read_messages` call. + +--- + +## Server terminal output + +Every `post_message` call prints a one-liner to stdout in the dedicated relay terminal: + +``` +[14:32:01] dev-b → pm [status] "Task P4 DONE, last commit abc1234..." +[14:33:15] pm → dev-b [directive] "PROCEED to task B1" +``` + +This log is the operational value of keeping the server in a dedicated terminal rather than backgrounding it. + +--- + +## Launcher script (`start.sh`) + +`start.sh` accepts one optional flag: + +| Flag | Behavior | +|------------|----------| +| *(default)*| `--manual` mode: prints three labeled prompt blocks (one per role) for copy-paste into fresh Claude Code sessions, then starts the server in the foreground | +| `--tmux` | Creates a new tmux window with four panes: relay server + PM + Dev-A + Dev-B, each pre-loaded with its kickoff command | +| `--kitty` | Same layout using kitty's `launch --new-tab` / `--new-window` | + +Execution order (all modes): + +1. `cd tools/relay && npm install --silent` (no-op if `node_modules` is current) +2. Print the session snippet (copy-paste blocks or multiplexer launch) +3. Foreground `npx tsx server.ts` — the terminal that ran `start.sh` becomes the relay terminal; no compile step needed + +Port-already-in-use check before step 3: if `:7331` is bound, print `relay already running? kill it with: kill $(lsof -ti:7331)` and exit 1. + +--- + +## Claude Code configuration + +Add to project `.claude/settings.json`: + +```json +"mcpServers": { + "relay": { + "type": "sse", + "url": "http://localhost:7331/sse" + } +} +``` + +This is project-scoped — the relay tools only appear in Relicario Claude Code sessions. When the server is not running, Claude Code shows a yellow MCP connection warning but does not break. Agents gracefully fall back to asking the user to relay manually (existing behavior). + +--- + +## Kickoff prompt changes + +One paragraph added near the top of each coordination prompt (`v0.5.0-pm-prompt.md`, `v0.5.0-dev-a-prompt.md`, `v0.5.0-dev-b-prompt.md` as template): + +> **Relay server:** A message-bus MCP server is running. You have three tools: `post_message(to, kind, body)`, `read_messages(for)`, `list_pending(for)`. Recipients: `pm`, `dev-a`, `dev-b`. Use these instead of asking the user to copy-paste. Before starting each task call `read_messages(for="")`. After emitting any status, question, or directive block, call `post_message` with `kind` set to the block type and `body` set to the formatted block. + +The `multi-agent-kickoff` skill is updated to: +- Remind the user to run `tools/relay/start.sh` before opening the three sessions +- Inject the relay paragraph automatically into every generated kickoff prompt + +--- + +## Error handling + +| Scenario | Behavior | +|----------|----------| +| Unknown `to` in `post_message` | MCP error returned; message not queued | +| Server crash / restart | In-flight messages lost; agent re-sends | +| Port 7331 in use at startup | Startup exits 1 with a kill hint | +| Session connects before server starts | Claude Code shows MCP warning; agent falls back to manual relay | + +No authentication. This is localhost-only, single-machine, dev-tool use. + +--- + +## Testing + +`queue.test.ts` using Node's built-in `node:test` runner. No extra test dep. + +Coverage: +- `post_message` + `read_messages` roundtrip (single and multiple messages) +- Consume-once: second `read_messages` on same inbox returns empty +- `list_pending` does not drain inbox +- FIFO ordering across multiple senders to the same inbox +- Unknown recipient returns an error + +No integration test against the MCP SSE transport — that is the SDK's responsibility. + +--- + +## What this is not + +- Not a product feature — never bundled with the extension or CLI +- Not persistent — no SQLite, no file queue, in-memory only +- Not authenticated — localhost dev tool, no threat model +- Not a general-purpose message bus — three hardcoded roles, no dynamic registration