diff --git a/docs/superpowers/plans/2026-05-02-relay-server.md b/docs/superpowers/plans/2026-05-02-relay-server.md new file mode 100644 index 0000000..7e592ae --- /dev/null +++ b/docs/superpowers/plans/2026-05-02-relay-server.md @@ -0,0 +1,956 @@ +# Relay Server Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Build a local MCP SSE server that gives PM, Dev-A, and Dev-B Claude Code sessions native `post_message`/`read_messages`/`list_pending` tools, eliminating manual copy-paste during multi-agent development lifts. + +**Architecture:** A single Node.js process hosts an HTTP server with SSE transport for the MCP protocol. Three named in-memory FIFO queues (one per role) hold consume-once messages. A `start.sh` launcher prints copy-paste instructions (default) or spawns a tmux/kitty layout (flags). The multi-agent-kickoff skill templates get a `<>` placeholder injected so every future lift prompt auto-includes relay instructions. + +**Tech Stack:** Node.js v25, `@modelcontextprotocol/sdk` (MCP + SSE transport), `tsx` (dev dep, runs TypeScript directly), Node built-in `node:test` runner. No Express, no Hono, no Zod as a direct dep. + +--- + +## File map + +| Action | Path | Responsibility | +|--------|------|----------------| +| Create | `tools/relay/package.json` | npm metadata, scripts, single runtime dep + tsx devDep | +| Create | `tools/relay/tsconfig.json` | TypeScript config for ESM Node target | +| Create | `tools/relay/queue.ts` | `RelayQueue` class — in-memory FIFO, `post`/`read`/`pending`, `isRole` guard | +| Create | `tools/relay/queue.test.ts` | Node `node:test` unit tests for queue (5 cases) | +| Create | `tools/relay/server.ts` | MCP `Server` + `SSEServerTransport` HTTP server on port 7331 | +| Create | `tools/relay/start.sh` | Launcher: `--manual` (default), `--tmux`, `--kitty` | +| Modify | `.gitignore` | Add `tools/relay/node_modules/` | +| Modify | `.claude/settings.json` | Add `mcpServers.relay` SSE entry | +| Create | `docs/superpowers/MULTI-AGENT.md` | Paradigm reference README | +| Modify | `~/.claude/skills/multi-agent-kickoff/templates/pm-prompt.md` | Add `<>` section | +| Modify | `~/.claude/skills/multi-agent-kickoff/templates/dev-prompt.md` | Add `<>` section | +| Modify | `~/.claude/skills/multi-agent-kickoff/SKILL.md` | Placeholder ref + step 8 update + `<>` placeholder | + +--- + +## Task 1: Scaffold `tools/relay/` + +**Files:** +- Create: `tools/relay/package.json` +- Create: `tools/relay/tsconfig.json` +- Modify: `.gitignore` + +- [ ] **Step 1: Create `tools/relay/package.json`** + +```json +{ + "name": "@relicario/relay", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "start": "npx tsx server.ts", + "test": "node --import=tsx/esm --test queue.test.ts" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.10.0" + }, + "devDependencies": { + "tsx": "^4.19.0", + "@types/node": "^22.0.0" + } +} +``` + +- [ ] **Step 2: Create `tools/relay/tsconfig.json`** + +```json +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "noEmit": true + }, + "include": ["*.ts"] +} +``` + +- [ ] **Step 3: Add to root `.gitignore`** + +Open `/home/alee/Sources/relicario/.gitignore` and append: + +``` +tools/relay/node_modules/ +``` + +- [ ] **Step 4: Install dependencies and verify** + +```bash +cd tools/relay && npm install +``` + +Expected: `node_modules/` created, no errors. Verify with: + +```bash +ls node_modules/@modelcontextprotocol/sdk && ls node_modules/tsx +``` + +Expected: both directories exist. + +- [ ] **Step 5: Commit scaffold** + +```bash +git add tools/relay/package.json tools/relay/tsconfig.json tools/relay/package-lock.json .gitignore +git commit -m "chore(relay): scaffold tools/relay with MCP SDK dep" +``` + +--- + +## Task 2: `queue.ts` — TDD + +**Files:** +- Create: `tools/relay/queue.ts` +- Create: `tools/relay/queue.test.ts` + +- [ ] **Step 1: Write the failing tests** + +Create `tools/relay/queue.test.ts`: + +```typescript +import { describe, it, beforeEach } from "node:test"; +import assert from "node:assert/strict"; +import { RelayQueue, isRole } from "./queue.ts"; + +describe("RelayQueue", () => { + let q: RelayQueue; + + beforeEach(() => { + q = new RelayQueue(); + }); + + it("post + read roundtrip returns the message with correct fields", () => { + q.post("dev-b", "pm", "status", "Task P4 DONE"); + const msgs = q.read("pm"); + assert.equal(msgs.length, 1); + assert.equal(msgs[0].from, "dev-b"); + assert.equal(msgs[0].to, "pm"); + assert.equal(msgs[0].kind, "status"); + assert.equal(msgs[0].body, "Task P4 DONE"); + assert.ok(typeof msgs[0].id === "string" && msgs[0].id.length > 0); + assert.ok(typeof msgs[0].ts === "string"); + }); + + it("consume-once: second read returns empty", () => { + q.post("dev-a", "pm", "question", "Should I use approach A?"); + q.read("pm"); + const second = q.read("pm"); + assert.deepEqual(second, []); + }); + + it("list_pending does not drain inbox", () => { + q.post("dev-b", "pm", "directive", "PROCEED"); + const before = q.pending("pm"); + assert.equal(before.count, 1); + const after = q.read("pm"); + assert.equal(after.length, 1); + }); + + it("FIFO ordering across multiple senders", () => { + q.post("dev-a", "pm", "status", "first"); + q.post("dev-b", "pm", "status", "second"); + q.post("dev-a", "pm", "question", "third"); + const msgs = q.read("pm"); + assert.equal(msgs.length, 3); + assert.equal(msgs[0].body, "first"); + assert.equal(msgs[1].body, "second"); + assert.equal(msgs[2].body, "third"); + }); + + it("isRole rejects unknown strings", () => { + assert.ok(isRole("pm")); + assert.ok(isRole("dev-a")); + assert.ok(isRole("dev-b")); + assert.ok(!isRole("dev-c")); + assert.ok(!isRole("")); + assert.ok(!isRole("PM")); + }); +}); +``` + +- [ ] **Step 2: Run tests to confirm they fail** + +```bash +cd tools/relay && node --import=tsx/esm --test queue.test.ts +``` + +Expected: fails with `Cannot find module './queue.ts'` or similar. If it fails with a different error, investigate before continuing. + +- [ ] **Step 3: Write `queue.ts`** + +Create `tools/relay/queue.ts`: + +```typescript +import { randomUUID } from "node:crypto"; + +export type Role = "pm" | "dev-a" | "dev-b"; +export type MessageKind = "status" | "question" | "directive" | "free"; + +export interface RelayMessage { + id: string; + from: Role; + to: Role; + kind: MessageKind; + body: string; + ts: string; +} + +const KNOWN_ROLES = new Set(["pm", "dev-a", "dev-b"]); + +export function isRole(s: string): s is Role { + return KNOWN_ROLES.has(s); +} + +export class RelayQueue { + private readonly queues = new Map([ + ["pm", []], + ["dev-a", []], + ["dev-b", []], + ]); + + post(from: Role, to: Role, kind: MessageKind, body: string): RelayMessage { + const msg: RelayMessage = { + id: randomUUID(), + from, + to, + kind, + body, + ts: new Date().toISOString(), + }; + this.queues.get(to)!.push(msg); + return msg; + } + + read(forRole: Role): RelayMessage[] { + const inbox = this.queues.get(forRole)!; + const messages = [...inbox]; + inbox.length = 0; + return messages; + } + + pending(forRole: Role): { count: number; kinds: MessageKind[] } { + const inbox = this.queues.get(forRole)!; + return { + count: inbox.length, + kinds: inbox.map((m) => m.kind), + }; + } +} +``` + +- [ ] **Step 4: Run tests to confirm they pass** + +```bash +cd tools/relay && node --import=tsx/esm --test queue.test.ts +``` + +Expected output (all 5 passing): +``` +▶ RelayQueue + ✔ post + read roundtrip returns the message with correct fields + ✔ consume-once: second read returns empty + ✔ list_pending does not drain inbox + ✔ FIFO ordering across multiple senders + ✔ isRole rejects unknown strings +▶ RelayQueue (Xms) +ℹ tests 5 +ℹ pass 5 +ℹ fail 0 +``` + +If any test fails, fix `queue.ts` before proceeding. + +- [ ] **Step 5: Commit** + +```bash +git add tools/relay/queue.ts tools/relay/queue.test.ts +git commit -m "feat(relay): in-memory queue with consume-once semantics" +``` + +--- + +## Task 3: `server.ts` + +**Files:** +- Create: `tools/relay/server.ts` + +- [ ] **Step 1: Write `server.ts`** + +Create `tools/relay/server.ts`: + +```typescript +import { Server } from "@modelcontextprotocol/sdk/server/index.js"; +import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"; +import { + CallToolRequestSchema, + ListToolsRequestSchema, +} from "@modelcontextprotocol/sdk/types.js"; +import http from "node:http"; +import { RelayQueue, isRole } from "./queue.ts"; + +const PORT = 7331; +const queue = new RelayQueue(); + +const mcpServer = new Server( + { name: "relay", version: "0.1.0" }, + { capabilities: { tools: {} } } +); + +const TOOLS = [ + { + name: "post_message", + description: + "Push a message to a recipient's inbox. Returns the assigned message id.", + inputSchema: { + type: "object" as const, + properties: { + from: { + type: "string", + enum: ["pm", "dev-a", "dev-b"], + description: "Your role name", + }, + to: { + type: "string", + enum: ["pm", "dev-a", "dev-b"], + description: "Recipient role name", + }, + kind: { + type: "string", + enum: ["status", "question", "directive", "free"], + description: "Message type matching the coordination protocol", + }, + body: { + type: "string", + description: "Message body — freeform markdown, typically the full formatted block", + }, + }, + required: ["from", "to", "kind", "body"], + }, + }, + { + name: "read_messages", + description: + "Pop and return all pending messages for this recipient. Inbox is empty after this call (consume-once).", + inputSchema: { + type: "object" as const, + properties: { + for: { + type: "string", + enum: ["pm", "dev-a", "dev-b"], + description: "Your role name", + }, + }, + required: ["for"], + }, + }, + { + name: "list_pending", + description: + "Return count and kinds of pending messages without consuming them.", + inputSchema: { + type: "object" as const, + properties: { + for: { + type: "string", + enum: ["pm", "dev-a", "dev-b"], + description: "Your role name", + }, + }, + required: ["for"], + }, + }, +]; + +mcpServer.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS })); + +mcpServer.setRequestHandler(CallToolRequestSchema, async (request) => { + const { name, arguments: args } = request.params; + const a = args as Record; + + if (name === "post_message") { + if (!isRole(a.from)) { + return { content: [{ type: "text" as const, text: `Error: unknown role "${a.from}"` }], isError: true }; + } + if (!isRole(a.to)) { + return { content: [{ type: "text" as const, text: `Error: unknown role "${a.to}"` }], isError: true }; + } + const kind = a.kind as "status" | "question" | "directive" | "free"; + const msg = queue.post(a.from, a.to, kind, a.body); + const ts = new Date(msg.ts).toTimeString().slice(0, 8); + const preview = a.body.slice(0, 60).replace(/\n/g, " "); + const ellipsis = a.body.length > 60 ? "..." : ""; + process.stdout.write(`[${ts}] ${a.from} → ${a.to} [${kind}] "${preview}${ellipsis}"\n`); + return { content: [{ type: "text" as const, text: JSON.stringify({ id: msg.id }) }] }; + } + + if (name === "read_messages") { + if (!isRole(a.for)) { + return { content: [{ type: "text" as const, text: `Error: unknown role "${a.for}"` }], isError: true }; + } + const messages = queue.read(a.for); + return { content: [{ type: "text" as const, text: JSON.stringify(messages) }] }; + } + + if (name === "list_pending") { + if (!isRole(a.for)) { + return { content: [{ type: "text" as const, text: `Error: unknown role "${a.for}"` }], isError: true }; + } + const result = queue.pending(a.for); + return { content: [{ type: "text" as const, text: JSON.stringify(result) }] }; + } + + return { + content: [{ type: "text" as const, text: `Error: unknown tool "${name}"` }], + isError: true, + }; +}); + +const transports = new Map(); + +const httpServer = http.createServer(async (req, res) => { + try { + if (req.method === "GET" && req.url === "/sse") { + const transport = new SSEServerTransport("/message", res); + transports.set(transport.sessionId, transport); + transport.onclose = () => transports.delete(transport.sessionId); + await mcpServer.connect(transport); + } else if (req.method === "POST" && req.url?.startsWith("/message")) { + const url = new URL(req.url, `http://127.0.0.1:${PORT}`); + const sessionId = url.searchParams.get("sessionId") ?? ""; + const transport = transports.get(sessionId); + if (transport) { + await transport.handlePostMessage(req, res); + } else { + res.writeHead(404, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ error: "session not found" })); + } + } else { + res.writeHead(404).end("not found"); + } + } catch (err) { + console.error("[relay] error:", err); + if (!res.headersSent) res.writeHead(500).end(String(err)); + } +}); + +httpServer.listen(PORT, "127.0.0.1", () => { + console.log(`[relay] server ready on :${PORT}`); + console.log(`[relay] tools: post_message, read_messages, list_pending`); + console.log(`[relay] waiting for connections — Ctrl-C to stop`); +}); +``` + +- [ ] **Step 2: Smoke-test server startup** + +In one terminal: +```bash +cd tools/relay && npx tsx server.ts +``` + +Expected output: +``` +[relay] server ready on :7331 +[relay] tools: post_message, read_messages, list_pending +[relay] waiting for connections — Ctrl-C to stop +``` + +In a second terminal, verify the port is listening: +```bash +curl -s --max-time 2 http://127.0.0.1:7331/sse | head -3 +``` + +Expected: SSE `data:` stream begins (it won't complete — the connection stays open). Ctrl-C both. + +If the server errors on startup, check that `@modelcontextprotocol/sdk` is installed and review any TypeScript errors by running `npx tsc --noEmit` in `tools/relay/`. + +- [ ] **Step 3: Commit** + +```bash +git add tools/relay/server.ts +git commit -m "feat(relay): MCP SSE server with post_message/read_messages/list_pending" +``` + +--- + +## Task 4: `start.sh` + +**Files:** +- Create: `tools/relay/start.sh` + +- [ ] **Step 1: Write `start.sh`** + +Create `tools/relay/start.sh`: + +```bash +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel)" +PORT=7331 +MODE="manual" + +for arg in "$@"; do + case "$arg" in + --tmux) MODE="tmux" ;; + --kitty) MODE="kitty" ;; + --manual) MODE="manual" ;; + *) echo "Unknown option: $arg" >&2; echo "Usage: $0 [--manual|--tmux|--kitty]" >&2; exit 1 ;; + esac +done + +# Port check +if lsof -ti:"$PORT" &>/dev/null; then + echo "Error: port $PORT is already in use." + echo "Relay already running? Kill it with: kill \$(lsof -ti:$PORT)" + exit 1 +fi + +# Install deps (no-op if node_modules current) +cd "$SCRIPT_DIR" +npm install --silent + +# Discover latest coordination prompts for instructions +COORD_DIR="$REPO_ROOT/docs/superpowers/coordination" +PM_PROMPT="$(ls -t "$COORD_DIR"/*-pm-prompt.md 2>/dev/null | head -1 || echo "(none found — run multi-agent-kickoff skill first)")" +DEV_A_PROMPT="$(ls -t "$COORD_DIR"/*-dev-a-prompt.md 2>/dev/null | head -1 || echo "(none found)")" +DEV_B_PROMPT="$(ls -t "$COORD_DIR"/*-dev-b-prompt.md 2>/dev/null | head -1 || echo "(none found)")" + +print_manual_instructions() { + echo "" + echo "╔══════════════════════════════════════════════════════════════╗" + echo "║ RELAY SERVER — MULTI-AGENT LIFT LAUNCHER ║" + echo "╚══════════════════════════════════════════════════════════════╝" + echo "" + echo "Open 3 new terminals. In each, start Claude Code and paste" + echo "the content BELOW the '---' line from the corresponding file." + echo "" + echo " Terminal 1 (PM): cat '$PM_PROMPT'" + echo " Terminal 2 (Dev A): cat '$DEV_A_PROMPT'" + echo " Terminal 3 (Dev B): cat '$DEV_B_PROMPT'" + echo "" + echo "This terminal becomes the relay log. Keep it open." + echo "" + echo "══════════════════════════════════════════════════════════════" +} + +launch_tmux() { + SESSION="relay-lift" + tmux new-session -d -s "$SESSION" -n "relay" \ + "cd '$SCRIPT_DIR' && npx tsx server.ts" + tmux new-window -t "$SESSION:" -n "pm" "cd '$REPO_ROOT' && claude" + tmux new-window -t "$SESSION:" -n "dev-a" "cd '$REPO_ROOT' && claude" + tmux new-window -t "$SESSION:" -n "dev-b" "cd '$REPO_ROOT' && claude" + echo "" + echo "[relay] Opened tmux session '$SESSION' with 4 windows: relay, pm, dev-a, dev-b." + echo "[relay] Paste the kickoff prompt into each Claude window." + echo " Prompts:" + echo " PM: $PM_PROMPT" + echo " Dev A: $DEV_A_PROMPT" + echo " Dev B: $DEV_B_PROMPT" + echo "" + tmux attach-session -t "$SESSION" +} + +launch_kitty() { + kitty @ launch --new-tab --tab-title "relay" -- \ + bash -c "cd '$SCRIPT_DIR' && npx tsx server.ts" + kitty @ launch --new-window --window-title "PM" -- \ + bash -c "cd '$REPO_ROOT' && claude" + kitty @ launch --new-window --window-title "Dev-A" -- \ + bash -c "cd '$REPO_ROOT' && claude" + kitty @ launch --new-window --window-title "Dev-B" -- \ + bash -c "cd '$REPO_ROOT' && claude" + echo "" + echo "[relay] Opened kitty tab 'relay' + 3 windows (PM, Dev-A, Dev-B)." + echo " Paste the kickoff prompts into each Claude window." + echo " PM: $PM_PROMPT" + echo " Dev A: $DEV_A_PROMPT" + echo " Dev B: $DEV_B_PROMPT" +} + +case "$MODE" in + manual) + print_manual_instructions + exec npx tsx "$SCRIPT_DIR/server.ts" + ;; + tmux) + launch_tmux + ;; + kitty) + launch_kitty + ;; +esac +``` + +- [ ] **Step 2: Make executable** + +```bash +chmod +x tools/relay/start.sh +``` + +- [ ] **Step 3: Smoke-test `--manual` mode** + +```bash +cd /home/alee/Sources/relicario && tools/relay/start.sh +``` + +Expected: prints the launch box with prompt paths, then server starts and shows `[relay] server ready on :7331`. Ctrl-C to stop. + +- [ ] **Step 4: Commit** + +```bash +git add tools/relay/start.sh +git commit -m "feat(relay): start.sh launcher with --manual/--tmux/--kitty modes" +``` + +--- + +## Task 5: Claude Code MCP configuration + +**Files:** +- Modify: `.claude/settings.json` + +- [ ] **Step 1: Read current `.claude/settings.json`** + +```bash +cat .claude/settings.json +``` + +- [ ] **Step 2: Add the relay MCP server entry** + +The file currently has `{ "enabledPlugins": { ... } }`. Add `"mcpServers"` at the top level: + +```json +{ + "mcpServers": { + "relay": { + "type": "sse", + "url": "http://localhost:7331/sse" + } + }, + "enabledPlugins": { + "superpowers@claude-plugins-official": true + } +} +``` + +Preserve whatever is already in `enabledPlugins` — only add the `mcpServers` key. + +- [ ] **Step 3: Commit** + +```bash +git add .claude/settings.json +git commit -m "chore(relay): add relay MCP server to project Claude config" +``` + +--- + +## Task 6: `docs/superpowers/MULTI-AGENT.md` + +**Files:** +- Create: `docs/superpowers/MULTI-AGENT.md` + +- [ ] **Step 1: Write the paradigm README** + +Create `docs/superpowers/MULTI-AGENT.md`: + +```markdown +# Multi-Agent Development Paradigm + +This repo uses a three-terminal workflow for large development lifts: one Claude Code session acts as **PM** and two act as **senior developers** (Dev-A, Dev-B), each working in their own git worktree on a parallel feature branch. + +A local relay MCP server eliminates manual message copying between terminals — agents call `post_message`/`read_messages` instead of asking the user to copy-paste. + +--- + +## Overview + +| Role | Terminal | Branch | Responsibilities | +|------|----------|--------|-----------------| +| PM | 1 | `main` (read-only) | Drive doc-audit follow-ups, review PRs, write CHANGELOG, authorize merges and tagging | +| Dev-A | 2 | `feature/-plan-a-*` | Implement Plan A tasks in their own worktree | +| Dev-B | 3 | `feature/-plan-b-*` | Implement Plan B tasks in their own worktree | +| Relay server | 4 | — | Message bus; Ctrl-C to stop at end of lift | + +**User's job:** authorize merges (the PM asks), resolve escalations the PM can't handle, and watch the streams. You are no longer the message bus. + +--- + +## Starting a lift + +### Prerequisites + +- [ ] Kickoff prompts exist in `docs/superpowers/coordination/` (generate with the `multi-agent-kickoff` skill if not) +- [ ] No uncommitted changes in main that would confuse the devs +- [ ] `tools/relay/` is present (run `ls tools/relay/` to confirm) + +### Launch sequence + +```bash +# 1. Start the relay server (this terminal becomes the relay log) +tools/relay/start.sh # prints copy-paste instructions, then starts server + +# Optional: use a multiplexer to auto-open all four terminals +tools/relay/start.sh --tmux # creates tmux session "relay-lift" with 4 windows +tools/relay/start.sh --kitty # creates kitty tab "relay" + 3 windows +``` + +`start.sh` prints the paths to the three kickoff prompt files. In each Claude Code terminal, run `cat ` and paste everything below the `---` line as the first message. + +--- + +## Coordination protocol + +Agents communicate by posting structured blocks to each other's inboxes. Four message kinds: + +| Kind | Block header | When used | +|------|-------------|-----------| +| `status` | `## STATUS UPDATE — DEV-*` | After completing a task, getting blocked, or reaching a review-ready state | +| `question` | `## QUESTION TO PM — DEV-*` | When a dev needs PM input mid-task | +| `directive` | `## DIRECTIVE TO DEV-*` | When PM instructs a dev to proceed, hold, rescope, or approve a PR | +| `free` | (none) | Ad-hoc messages not covered by the above | + +A well-formed `status` block: + +``` +## STATUS UPDATE — DEV-B +Time: 2026-05-02T14:30:00-07:00 +Branch: feature/v0.5.0-plan-b-extension-ux +Task: P4 / error-copy map +Status: DONE +Last commit: abc1234 feat(extension): centralize ERROR_COPY map +Tests: green +Notes: No issues. Ready for PM review of P4 before starting B1. +``` + +--- + +## Using the relay tools + +All three Claude Code sessions have these tools available when the relay server is running: + +``` +post_message(from, to, kind, body) → { id } +read_messages(for) → RelayMessage[] (drains inbox) +list_pending(for) → { count, kinds } (non-destructive) +``` + +Typical dev flow per task: + +``` +1. read_messages(for="dev-b") # check for directives before starting +2. ... do the work ... +3. post_message(from="dev-b", to="pm", kind="status", body="## STATUS UPDATE...") +``` + +Typical PM flow: + +``` +1. read_messages(for="pm") # see what devs posted +2. ... review ... +3. post_message(from="pm", to="dev-b", kind="directive", body="## DIRECTIVE TO DEV-B...") +``` + +--- + +## If the relay server isn't running + +Claude Code will show a yellow MCP connection warning for the `relay` server. The tools will be unavailable. + +Agents fall back to the manual protocol: they emit the structured blocks as text and ask the user to copy-paste them to the relevant terminal. This is slower but fully functional — the coordination protocol works either way. + +To restart a crashed server mid-lift: + +```bash +tools/relay/start.sh +``` + +In-flight messages are lost on restart. Any agent with unread messages should re-post them. + +--- + +## Generating kickoff prompts + +Use the `multi-agent-kickoff` skill (in the `superpowers` plugin). It auto-discovers the spec and plans for the release, substitutes all placeholders including the relay paragraph, and writes files to `docs/superpowers/coordination/`. + +The skill reminder: run `tools/relay/start.sh` **before** opening the three Claude Code sessions — the MCP tools need the server to be up when each session initializes. + +--- + +## Ending a lift + +1. PM emits `REVIEW-COMPLETE` and `MERGE-APPROVED` for each dev's PR +2. User merges each PR (the PM session does `gh pr merge` with user authorization) +3. PM tags the release (only after explicit user `yes`) +4. Ctrl-C the relay terminal — all in-memory messages are discarded + +--- + +## Roles and boundaries (quick reference) + +**PM must not:** write feature code, merge without user authorization, tag without user approval, run `git push --force` / `git reset --hard` without asking. + +**Devs must not:** merge their branch to main, push `--force`, run `git reset --hard` without asking. + +**User must:** authorize all merges and the release tag. Everything else is delegated. +``` + +- [ ] **Step 2: Commit** + +```bash +git add docs/superpowers/MULTI-AGENT.md +git commit -m "docs: add multi-agent development paradigm README" +``` + +--- + +## Task 7: Update `multi-agent-kickoff` skill + +**Files:** +- Modify: `~/.claude/skills/multi-agent-kickoff/templates/pm-prompt.md` +- Modify: `~/.claude/skills/multi-agent-kickoff/templates/dev-prompt.md` +- Modify: `~/.claude/skills/multi-agent-kickoff/SKILL.md` + +- [ ] **Step 1: Read current templates** + +```bash +cat ~/.claude/skills/multi-agent-kickoff/templates/pm-prompt.md +cat ~/.claude/skills/multi-agent-kickoff/templates/dev-prompt.md +``` + +Note where the "Setup" section ends in each template. The relay paragraph goes right after it (before "Required reading"). + +- [ ] **Step 2: Add `<>` to `pm-prompt.md`** + +In `~/.claude/skills/multi-agent-kickoff/templates/pm-prompt.md`, find the "## Setup" section and add the placeholder block immediately after it (before the "## Required reading" heading): + +```markdown +<> +``` + +The generated output for this placeholder (substituted by the skill at generation time) is: + +```markdown +## Relay server + +A message-bus MCP server is running on `localhost:7331`. You have three native tools: + +- `post_message(from, to, kind, body)` — push a message; `from` is always `"pm"` for you +- `read_messages(for)` — drain your inbox; call with `for="pm"` before each action +- `list_pending(for)` — check inbox count without consuming + +Recipients: `pm`, `dev-a`, `dev-b`. Use these instead of asking the user to copy-paste. After sending any directive, call `post_message(from="pm", to="dev-a", kind="directive", body="...")`. +``` + +- [ ] **Step 3: Add `<>` to `dev-prompt.md`** + +In `~/.claude/skills/multi-agent-kickoff/templates/dev-prompt.md`, find the "## Setup" section and add immediately after it (before "## Required reading"): + +```markdown +<> +``` + +The generated output for this placeholder is role-specific (uses `<>`): + +```markdown +## Relay server + +A message-bus MCP server is running on `localhost:7331`. You have three native tools: + +- `post_message(from, to, kind, body)` — push a message; your `from` is always `"<>"` +- `read_messages(for)` — drain your inbox; call with `for="<>"` before each task +- `list_pending(for)` — check inbox count without consuming + +Recipients: `pm`, `dev-a`, `dev-b`. Use these instead of asking the user to copy-paste. Before starting each task: `read_messages(for="<>")`. After emitting any status/question block: `post_message(from="<>", to="pm", kind="status"|"question", body="...")`. +``` + +- [ ] **Step 4: Update `SKILL.md` — add two entries to the placeholder reference table** + +In `~/.claude/skills/multi-agent-kickoff/SKILL.md`, find the "### Common to all prompts" section of the Placeholder reference and add: + +```markdown +- `<>` — the relay server instruction block (substituted from the template above). For the PM prompt, `from` is hardcoded to `"pm"`. For dev prompts, uses `<>`. +``` + +In the "### Per-dev" section, add: + +```markdown +- `<>` — lowercase relay role name, e.g. `dev-a`, `dev-b`. Derived from `<>` by lowercasing and prepending `dev-`. Set when `<>` is set. +``` + +- [ ] **Step 5: Update `SKILL.md` — step 8 (kickoff instructions)** + +Find step 8 in the Process section ("Print kickoff instructions") and prepend a bullet: + +```markdown +8. **Print kickoff instructions.** Tell the user exactly what to do: + - **Start the relay server first:** `tools/relay/start.sh` (or `--tmux`/`--kitty` for auto-layout). The server must be running before the sessions open so the MCP tools initialize correctly. + - Open three terminal windows (or panes — their choice of multiplexer) + ...rest of existing bullets unchanged... +``` + +Also update the "After generation" section — change bullet 4 ("From that point on, they're the message bus...") to: + +```markdown +4. The relay server handles message routing — agents call `post_message`/`read_messages` directly. The user only needs to step in for escalations the PM can't resolve, or if the relay server is down (manual fallback: copy-paste the block to the relevant terminal as before) +``` + +- [ ] **Step 6: Commit skill changes** + +The skill files live outside the git repo, so no git commit needed. Verify the changes look right: + +```bash +grep -n "RELAY_PARAGRAPH\|DEV_ROLE\|relay server" ~/.claude/skills/multi-agent-kickoff/SKILL.md | head -10 +grep -n "RELAY_PARAGRAPH" ~/.claude/skills/multi-agent-kickoff/templates/pm-prompt.md +grep -n "RELAY_PARAGRAPH" ~/.claude/skills/multi-agent-kickoff/templates/dev-prompt.md +``` + +Expected: each grep returns at least one match. + +--- + +## Final verification + +- [ ] **Run queue tests one more time from repo root** + +```bash +cd tools/relay && node --import=tsx/esm --test queue.test.ts +``` + +Expected: 5 passing, 0 failing. + +- [ ] **Start server and verify it binds** + +```bash +tools/relay/start.sh & +sleep 1 +curl -s --max-time 1 http://127.0.0.1:7331/sse | head -1 || true +kill %1 +``` + +Expected: `data:` line appears (SSE stream started), then server killed cleanly. + +- [ ] **Verify MCP config is present** + +```bash +python3 -c "import json; d=json.load(open('.claude/settings.json')); print(d['mcpServers']['relay'])" +``` + +Expected: `{'type': 'sse', 'url': 'http://localhost:7331/sse'}` + +- [ ] **Verify skill placeholders were added** + +```bash +grep "RELAY_PARAGRAPH" ~/.claude/skills/multi-agent-kickoff/templates/pm-prompt.md \ + ~/.claude/skills/multi-agent-kickoff/templates/dev-prompt.md +``` + +Expected: one match per file.