runno-ai

chatnut

Use when spawning agent teams that need shared discussion visibility beyond hub-and-spoke DMs

runno-ai 2 1 Updated 4w ago

Resources

13
GitHub

Install

npx skillscat add runno-ai/chatnut

Install via the SkillsCat registry.

SKILL.md

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:

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

    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:

    # 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"}