---
name: chatnut
title: chatnut
description: Use when spawning agent teams that need shared discussion visibility beyond hub-and-spoke DMs
aliases: [chatnut]
---


# Team Chat

Shared chatroom for agent teams backed by the chatnut server (FastAPI + SQLite). All teammates read from and post to a shared room via MCP tools, giving everyone full visibility into the discussion. Includes a live web UI with SSE streaming for real-time observation.

**Storage:** SQLite database at `~/.chatnut/chatnut.db` (WAL mode). Safe from Claude Code's `TeamDelete`.

**Server:** Always running at your configured URL. MCP endpoint at `/mcp/`.

## Setup

After `TeamCreate`, initialize the chatroom and capture the **room_id** for teammates:

```
result = init_room(project="<project-name>", name="<team-name>", branch="<branch-name>", description="...")
# result contains: { "id": "<room-uuid>", "name": "...", "project": "...", ... }
# Pass result["id"] as ROOM_ID to all teammate spawn prompts
```

- **`project`** = the project being worked on (e.g., `my-app`, `backend`) — NOT the team name
- **`name`** = the chatroom name — naming convention is up to your team (e.g., `plan-auth-refactor`, `review-api-v2`)
- **`branch`** = the git branch being worked on (optional)

The returned `id` is a stable UUID. Pass it to teammates so they can use `room_id` for all reads/writes (faster, no name lookup).

The web UI is automatically opened in the user's browser when `init_room` is called. The response includes a `web_url` field with the direct link.

## Agent Registration (for @mention notifications)

After creating a room and spawning teammates, register each agent for @mention support:

```
register_agent(room_id=ROOM_ID, agent_name="security", task_id="security-agent-task-id")
register_agent(room_id=ROOM_ID, agent_name="architect", task_id="architect-agent-task-id")
```

When any teammate posts a message containing `@security`, the `post_message` response includes:
```json
{"mentions": [{"name": "security", "task_id": "security-agent-task-id"}]}
```

The PM (or posting agent) should then `SendMessage` to each mentioned agent's task_id.

- `agent_name` is case-insensitive (normalized to lowercase)
- Unregistered @mentions are silently skipped
- UPSERT semantics — re-registering updates the `task_id`

## Server Recovery

If any `mcp__chatnut__*` tool call fails with a connection or session error:

1. **Check server health:**

   ```bash
   PORT=$(cat ~/.chatnut/server.port 2>/dev/null || echo "8000")
   curl -s "http://127.0.0.1:${PORT}/api/status"
   ```

2. **If unreachable, restart the server:**

   ```bash
   # Graceful stop (if PID file exists):
   kill -TERM $(cat ~/.chatnut/server.pid 2>/dev/null) 2>/dev/null || true
   # Start in background:
   chatnut serve &
   ```
3. **Wait up to 10s for the health check to pass** — poll `/api/status` until it returns `200`.
4. **Retry the failed tool call once.**
5. **Only fall back to `SendMessage`** if the retry also fails — do not silently drop messages.

## MCP Tools

| Tool | Purpose |
|------|---------|
| `post_message(room_id, sender, content, message_type?)` | Post a message to a room |
| `read_messages(room_id, since_id?, limit?, message_type?)` | Read messages from a room |
| `wait_for_messages(room_id, since_id, timeout?, limit?, message_type?)` | Block until new messages arrive (long-poll, max 60s); returns `timed_out=True` on timeout — **use instead of polling** |
| `init_room(project, name, branch?, description?, team_name?)` | Create a room, returns room_id UUID; writes chatroom.json to team config dir when team_name provided |
| `list_rooms(project?, status?)` | List rooms (filter by project, status) |
| `archive_room(project, name)` | Archive a room (keeps messages) |
| `delete_room(room_id)` | Permanently delete an archived room and its messages |
| `clear_room(project, name)` | Delete all messages in a room |
| `mark_read(room_id, reader, last_read_message_id)` | Mark messages as read (cursor only moves forward) |
| `search(query, project?)` | Search room names + message content |
| `list_projects()` | List distinct project names |
| `ping()` | Health check — returns `db_path`, `status`, `version`, and optionally `latest` + `update_available` when a newer version exists |
| `update_status(room_id, sender, status)` | Update a sender's current status in a room (UPSERT) |
| `get_team_status(room_id)` | Get current status of all team members in a room |
| `register_agent(room_id, agent_name, task_id)` | Register an agent for @mention notifications (UPSERT, case-insensitive) |
| `list_agents(room_id)` | List all registered agents in a room |

## Communication Protocol

### Channels

```
SendMessage = wake-up ping (triggers a teammate's turn)
Chatroom    = content channel (all substantive discussion)
```

**Rule:** SendMessage contains only a short ping (e.g., "Check the chatroom — new findings posted"). ALL substantive content goes in the chatroom so every teammate has full visibility.

### Proactive Engagement Rules

Teammates are **active participants**, not passive responders:

1. **Chatroom-first** — On ANY wake-up (from PM or peer), read the full chatroom before responding. React to everything new, not just the ping that woke you.
2. **Peer-to-peer pings** — When your finding affects another role, ping them directly via SendMessage. Don't route through PM.
3. **@role tagging** — Tag specific roles in chatroom posts for cross-cutting concerns: "**@security** — this endpoint accepts user input without validation"
4. **Challenge and disagree** — Don't just agree. Propose alternatives, flag risks, question assumptions.
5. **Build on others** — Reference and extend other teammates' points. "Agreeing with @backend-dev on X — adding that Y is also affected."

### PM Role

The PM is a **facilitator**, not a message router:
- **Triggers rounds** — pings teammates to start each discussion round
- **Moderates** — posts pointed questions to the chatroom to drive discussion
- **Does NOT relay** — teammates ping each other directly for cross-cutting concerns
- **Synthesizes** — reads the full chatroom after rounds complete and consolidates findings

## Discussion Rounds

Consuming skills trigger rounds; the protocol within each round is standard.

**Round 1 — Initial Posts:**
PM posts context to chatroom → pings all teammates. Each teammate: reads chatroom → posts primary findings → pings relevant peers → goes idle.

**Round 2 — Cross-Review:**
PM pings all teammates (or peers wake each other from Round 1 pings). Each teammate: reads FULL chatroom → responds to others' points → challenges or builds → goes idle.

**Round 3 — Resolution** (optional, PM triggers if disagreements remain):
PM pings specific teammates with pointed questions. Teammates resolve directly via chatroom + peer pings.

**When to skip rounds:** Consuming skills may use fewer rounds (e.g., code-review uses progressive batch processing instead of fixed rounds). The protocol above is the default — skills adapt as needed.

## Teammate Instructions

Teammate protocol rules are installed as a global Claude Code rule at `~/.claude/rules/chatnut-protocol.md`. All spawned agents load this automatically — **no need to include protocol rules in spawn prompts**.

Install/update: `chatnut install`

The only thing consuming skills need to include in spawn prompts is the `room_id`:

```
## Team Chatroom (room_id: <ROOM_ID>)
```

### Multi-Round Protocol
You may be woken up multiple times:
- **Round 1:** Post your primary findings
- **Round 2:** Read others' posts and respond — agree, challenge, or extend
- **Round 3:** Resolve remaining disagreements

## MCP Fallback (SendMessage)

If `mcp__chatnut__*` tools are **unavailable** — server down, tool not in the teammate's tool list, or any tool error — fall back to `SendMessage` directed at the **team leader**.

### Detection

A teammate should switch to fallback mode when:
- Any `mcp__chatnut__*` call returns an error or is not available
- The `mcp__chatnut__ping` health check fails
- The tool is simply absent from the teammate's tool list

### Fallback Behavior (Teammate)

1. **Send full content** — do NOT trim to a ping; include everything the chatroom post would have contained
2. **Prefix with `[CHATROOM FALLBACK]`** — signals to the leader that MCP is down for this agent
3. **Direct to team leader** — always send to the PM/orchestrator, not peers
4. **Continue working** — one failed MCP call does not stop the task; proceed and report via SendMessage

```
SendMessage(
  type="message",
  recipient="<team-leader-name>",
  content="[CHATROOM FALLBACK] mcp__chatnut unavailable.\n\n## My Findings\n\n<full content>",
  summary="<role> findings (MCP fallback)"
)
```

### PM Handling of Fallback Messages

When the PM receives a `[CHATROOM FALLBACK]` message:
1. **If MCP is available on PM's end** — relay the content to the chatroom via `post_message` on behalf of the teammate, then continue normally
2. **If MCP is also down** — incorporate findings directly into PM's own work; note the teammate's contribution in any final summary
3. **Do NOT ignore** — fallback messages carry real work output, treat them as chatroom posts

## Team Lifecycle (PM Rules)

### Dismissing Teammates

1. **Check before dismissing** — before sending `shutdown_request`, check if the teammate is still working (in_progress tasks, recent chatroom posts). If they are, wait up to 1 minute for them to finish before proceeding with your own work.
2. **Partial dismissal is fine** — if some mates are done and others are still working, dismiss the finished ones and keep going. A team is not "done" until ALL mates are dismissed.
3. **Incorporate before dismissing** — when your current job completes, read new chatroom messages, incorporate findings, then dismiss mates whose work is complete. Don't dismiss blindly.

### PM Message Loop

After each piece of PM work completes:
1. Read new chatroom messages (`read_messages` with `since_id`)
2. Incorporate new findings into the ongoing work
3. Dismiss teammates who have completed their tasks (`shutdown_request`)
4. Continue with next PM task
5. Repeat until all work is done and all teammates are dismissed

### Teardown (Last Step Only)

**Archive the chatroom ONLY as the very last step** — after ALL teammates have been dismissed and all work is incorporated. Never archive while teammates are still active.

```
# 1. Verify ALL teammates are dismissed (no active members)
# 2. Archive chatroom (via MCP — do NOT stop server, it's persistent):
archive_room(project="<project-name>", name="<team-name>")
# 3. Finally:
TeamDelete
```

Archives are browsable in the web UI sidebar.

## Web UI

The server runs persistently at your configured URL. Features:

- **Real-time streaming** — messages appear as agents post them via SSE
- **Sidebar** — browse live and archived chatrooms (push-updated via SSE)
- **Markdown rendering** — code blocks with syntax highlighting, tables, lists
- **Auto-scroll** — follows new messages; pauses when scrolled up with "N new messages" pill
- **Dark mode** — dark-only UI optimized for developer use
- **Auto-reconnect** — EventSource reconnects with Last-Event-ID to avoid duplicates
- **Project filtering** — filter rooms by project in the sidebar

## Storage

**Prod DB:** `~/.chatnut/chatnut.db` — always-on service, never modified by `ss`.

**Dev DB:** `data/dev.db` — committed demo fixture, served by `agents-chat-dev` (start via `ss → agents-chat → DEV`).
- Seed/reset: `cd app/be && uv run python ../../data/seed.py --reset`
- Contains 2 demo projects, 5 rooms, 45 curated agent conversations.

SQLite database (WAL mode):

- **Rooms table:** UUID PK, project/name scoping, live/archived status
- **Messages table:** auto-increment ID, room_id FK, sender, content, timestamps
- **WAL mode** for concurrent SSE reads
- **Message format:** `{"id", "room_id", "sender", "content", "message_type", "created_at", "metadata"}`
