Files
relicario/docs/superpowers/plans/2026-05-02-relay-server.md
adlee-was-taken c0921b134d docs(plan): relay server implementation plan
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-02 16:44:06 -04:00

30 KiB
Raw Permalink Blame History

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 <<RELAY_PARAGRAPH>> 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 <<RELAY_PARAGRAPH>> section
Modify ~/.claude/skills/multi-agent-kickoff/templates/dev-prompt.md Add <<RELAY_PARAGRAPH>> section
Modify ~/.claude/skills/multi-agent-kickoff/SKILL.md Placeholder ref + step 8 update + <<DEV_ROLE>> 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

{
  "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
{
  "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
cd tools/relay && npm install

Expected: node_modules/ created, no errors. Verify with:

ls node_modules/@modelcontextprotocol/sdk && ls node_modules/tsx

Expected: both directories exist.

  • Step 5: Commit scaffold
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:

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
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:

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<string>(["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<Role, RelayMessage[]>([
    ["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
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
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:

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<string, string>;

  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<string, SSEServerTransport>();

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:

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:

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
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:

#!/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
chmod +x tools/relay/start.sh
  • Step 3: Smoke-test --manual mode
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
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

cat .claude/settings.json
  • Step 2: Add the relay MCP server entry

The file currently has { "enabledPlugins": { ... } }. Add "mcpServers" at the top level:

{
  "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
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:

# 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/<release>-plan-a-*` | Implement Plan A tasks in their own worktree |
| Dev-B | 3 | `feature/<release>-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 <path> 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:

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

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 <<RELAY_PARAGRAPH>> 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):

<<RELAY_PARAGRAPH>>

The generated output for this placeholder (substituted by the skill at generation time) is:

## 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 <<RELAY_PARAGRAPH>> 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"):

<<RELAY_PARAGRAPH>>

The generated output for this placeholder is role-specific (uses <<DEV_ROLE>>):

## 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 `"<<DEV_ROLE>>"`
- `read_messages(for)` — drain your inbox; call with `for="<<DEV_ROLE>>"` 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="<<DEV_ROLE>>")`. After emitting any status/question block: `post_message(from="<<DEV_ROLE>>", 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:

- `<<RELAY_PARAGRAPH>>` — the relay server instruction block (substituted from the template above). For the PM prompt, `from` is hardcoded to `"pm"`. For dev prompts, uses `<<DEV_ROLE>>`.

In the "### Per-dev" section, add:

- `<<DEV_ROLE>>` — lowercase relay role name, e.g. `dev-a`, `dev-b`. Derived from `<<DEV_LETTER>>` by lowercasing and prepending `dev-`. Set when `<<DEV_LETTER>>` 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:

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:

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:

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
cd tools/relay && node --import=tsx/esm --test queue.test.ts

Expected: 5 passing, 0 failing.

  • Start server and verify it binds
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
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
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.