Use when spawning agent teams that need shared discussion visibility beyond hub-and-spoke DMs
Resources
13Install
npx skillscat add runno-ai/chatnut Install via the SkillsCat registry.
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 promptsproject= the project being worked on (e.g.,my-app,backend) — NOT the team namename= 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:
{"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_nameis 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:
Check server health:
PORT=$(cat ~/.chatnut/server.port 2>/dev/null || echo "8000") curl -s "http://127.0.0.1:${PORT}/api/status"If unreachable, restart the server:
# Graceful stop (if PID file exists): kill -TERM $(cat ~/.chatnut/server.pid 2>/dev/null) 2>/dev/null || true # Start in background: chatnut serve &Wait up to 10s for the health check to pass — poll
/api/statusuntil it returns200.Retry the failed tool call once.
Only fall back to
SendMessageif 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__pinghealth check fails - The tool is simply absent from the teammate's tool list
Fallback Behavior (Teammate)
- Send full content — do NOT trim to a ping; include everything the chatroom post would have contained
- Prefix with
[CHATROOM FALLBACK]— signals to the leader that MCP is down for this agent - Direct to team leader — always send to the PM/orchestrator, not peers
- 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:
- If MCP is available on PM's end — relay the content to the chatroom via
post_messageon behalf of the teammate, then continue normally - If MCP is also down — incorporate findings directly into PM's own work; note the teammate's contribution in any final summary
- Do NOT ignore — fallback messages carry real work output, treat them as chatroom posts
Team Lifecycle (PM Rules)
Dismissing Teammates
- 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. - 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.
- 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:
- Read new chatroom messages (
read_messageswithsince_id) - Incorporate new findings into the ongoing work
- Dismiss teammates who have completed their tasks (
shutdown_request) - Continue with next PM task
- 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:
TeamDeleteArchives 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"}