Use when spawning agent teams that need shared discussion visibility beyond hub-and-spoke DMs
Scanned 5/29/2026
Install via CLI
openskills install runno-ai/chatnut---
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 |
## 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.
## Discussion Protocol Is Consumer-Defined
chatnut provides the **wire** (post / read / wait / mentions / status / fallback). It does NOT prescribe a discussion protocol. Consuming skills (e.g. `/plan-draft`, `/code-review`, `/research`) decide their own:
- **How many rounds** (zero / one / several / free-discuss with no rounds)
- **Who facilitates** (orchestrator-in-room / orchestrator-silent / no orchestrator)
- **How discussion ends** (DONE handshake / orchestrator-declared / timeout-only / human-in-loop / **quiescence-poll** — used by `/plan-draft`)
- **Engagement style** (debate-heavy challenge-and-build vs independent parallel verdicts vs single-shot reviews)
- **Triage / synthesis pattern** (orchestrator reads chatroom / spawns a triage subagent / consumes per-teammate DM summaries)
- **Lifecycle ordering** (when to archive, when to teardown relative to artifact updates)
For each of these, read the SKILL.md of the consuming skill.
The only protocol-level rules shipped at the chatnut layer are the **defensive primitives** (read-before-write, @mention dual-post, status reporting, idle-is-informational, MCP fallback) — see `chatnut-protocol.md`. These are wire-truth, not policy.
### Examples of consumer choices
| Consumer | Protocol shape |
|---|---|
| `/plan-draft` | Free-discuss (no rounds), orchestrator-silent (PM posts and reads ZERO messages from init_room until TeamDelete), no completion handshake, external quiescence-poll termination (90s silence), opus triage subagent reads disk-dump |
| Future skills | Whatever fits their problem |
If you're authoring a new consumer, you don't have to ask chatnut for permission to invent your protocol — chatnut is wire, you bring policy.
## Teammate Spawn Prompts (consumer-side)
Consuming skills define their spawn prompts. The only chatnut-level
requirement is to include the `room_id` so teammates can use the wire:
```
## Team Chatroom (room_id: <ROOM_ID>)
```
The defensive primitives in `chatnut-protocol.md` are auto-loaded into
every spawned agent's context — consumers don't need to repeat them in
spawn prompts. Consumers DO need to include their own protocol rules
(rounds vs free-discuss, completion semantics, engagement style) since
those are not shipped at the chatnut layer.
Install / update the global rule: `chatnut install`
## 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"}`
No comments yet. Be the first to comment!