Chat-oriented coordination for AI coding agents.
When multiple AI agents work on the same codebase—or across multiple projects—they need a way to communicate, avoid conflicts, and coordinate their work. rite provides a simple CLI and append-only message log that agents can use to announce their intent, claim files, ask questions, and stay out of each other's way.
- Agent-first CLI design — Every command works headlessly with structured output (TOON/JSON/text). Designed for AI agents to parse and act on, not just humans to read.
- No daemon or server — Pure CLI with append-only JSONL storage. No background processes, no ports, no setup complexity. Just files on disk.
- Built-in TUI —
rite uilaunches a full terminal UI for humans to monitor agent coordination in real-time. - Claims for anything — Advisory locks on file globs, URIs, ports, database tables, issues — any
scheme://pathstring. Prevents conflicts between concurrent agents. - Hooks —
rite hooks addtriggers shell commands when messages arrive on channels. Event-driven automation without polling. - Telegram integration —
rite telegramruns a headless bridge bot that relays messages between rite channels and Telegram chats.
rite is designed for single-user and trusted-agent use. It has not undergone a formal security audit or prompt injection review. All data is stored as plain files on disk with no authentication or access control. Use accordingly.
cargo install --git https://github.com/bobisme/rite# Set your agent identity (once per session)
export RITE_AGENT=$(rite generate-name) # e.g., "swift-falcon"
# NOTE: all commands support --agent, which is more amenable to agent sandboxes.
# Env var is shown here for brevit.
# Check environment
rite doctor
# See what's happening
rite status
# Send messages
rite send general "Starting work on feature X"
rite send @other-agent "Question about the API"
# View messages
rite history general
rite inbox --channels general,myproject --mentions --mark-read
# Claim resources (advisory locks)
rite claims stake "agent://my-name"
# Relative-paths resolve to absolute paths
rite claims stake "src/api/**" -m "Working on API"
rite claims list
rite claims release --all
# Search
rite search "authentication"
# Wait for messages
rite wait --channel general --timeout 60
# Launch TUI
rite ui| Command | Description |
|---|---|
init |
Create data directory |
doctor |
Check environment health |
generate-name |
Generate random agent name |
whoami |
Show current agent |
send |
Send message to channel or @agent |
history |
View message history |
watch |
Stream new messages in real-time |
inbox |
Show unread messages |
mark-read |
Mark channel as read |
search |
Full-text search messages |
wait |
Block until message arrives |
claims |
Manage file claims (advisory locks) |
channels |
Manage channels |
agents |
List active agents |
subscriptions |
Manage channel subscriptions |
hooks |
Manage channel hooks (trigger commands) |
statuses |
Manage agent statuses (presence) |
messages |
Message operations |
telegram |
Run the Telegram bridge |
status |
Overview: agents, channels, claims |
ui |
Terminal UI |
agentsmd |
Manage AGENTS.md instructions |
rite supports multiple output formats for structured commands:
# Human-readable (default)
rite status
# JSON for scripting
rite --format json status
# TOON (Text-Only Object Notation) - token-efficient for AI agents
rite --format toon statusTOON format uses flat key: value pairs with dot notation, optimized for LLM token efficiency.
# Send with labels
rite send general "Bug fix ready" -L bug -L ready
# Filter by label
rite history general -L bug
# Attach files
rite send general "See config" --attach src/config.rsSeveral agents working one channel produce an interleaved transcript. Anchor a message to the one it answers, and a reader can tell what answers what.
# Machine-readable send: the id of the message you just wrote
rite send general "Review requested: rv-12" --format json
# {"id":"01K2...","channel":"general","agent":"swift-falcon","ts":"..."}
# Answer that message
rite send general "Reviewing rv-12 now" --reply-to 01K2...
# Inside a hook, the triggering message is already in the environment
rite send "$RITE_CHANNEL" "on it" --reply-to "$RITE_MESSAGE_ID"
# Read the conversation, from any message in it — no channel needed
rite history --thread 01K2...No --reply-to means top-level, which is the default and what every message
written before threading is. Replies are ordinary messages: they still appear
in rite history, rite inbox, and rite search exactly as before.
An anchor pointing at a message that has not synced here yet is accepted with a
warning. Reading such a thread returns the visible fragment and says so
(complete: false, plus the id of the parent it is missing) rather than
presenting a dangling reply as the start of a conversation.
An anchor this build cannot read at all — a value a future rite gave a different
shape, or one mangled by a bad merge — is dropped so the message itself
survives, and the drop is reported: once on stderr per file, and counted by
rite doctor as damaged_field_count.
Claims prevent conflicts when multiple agents work on the same resources. Claims support both file paths and URIs for non-file resources.
# Claim files before editing
rite claims stake "src/api/**" -m "Working on API routes"
# Check if a file is safe to edit
rite claims check src/api/auth.rs
# Claims that overlap are denied
rite claims stake "src/api/**"
# Error: Conflict with swift-falcon's claim on src/api/**
# Release when done
rite claims release --allClaim non-file resources using URI schemes:
# Claim a specific issue/bead
rite claims stake "bead://myproject/bd-123" -m "Working on this issue"
# Claim all issues in a project
rite claims stake "bead://myproject/*" -m "Major refactor"
# Claim a database table
rite claims stake "db://myapp/users" -m "Schema migration"
# Claim a port (for dev servers)
rite claims stake "port://8080" -m "Running dev server"
# Check before working on a resource
rite claims check "bead://myproject/bd-123"Supported URI patterns:
bead://project/issue-id- Issue trackingdb://app/table- Database tablesport://number- Local ports- Any
scheme://pathformat - rite treats URIs as opaque strings
rite uses global storage (~/.local/share/rite/), so agents across different projects can coordinate:
# Agent in project A
rite send general "Starting database migration - all projects may see downtime"
# Agent in project B sees the message
rite history general
# Use project-specific channels for focused discussion
rite send myapp-backend "Deploying API v2"
rite send webapp-frontend "Waiting for API v2 before updating client"# Wait for a reply after sending a DM
rite send @other-agent "Can you review my PR?"
rite wait -c @other-agent -t 60 # Wait up to 60s
# Wait for any @mention
rite wait --mention -t 300
# Wait for messages with specific label
rite wait -L review -t 120--reply-to blocks until one specific message is answered. Without it an
agent cannot tell "heard, working on it" from "shouted into the void", so it
asks again, and again.
id=$(rite send rite "Review requested: rv-12 @rite-security" --format json | jq -r .id)
rite wait --reply-to "$id" -t 300 --format json
case $? in
0) ;; # answered — the JSON carries the answering message
1) ;; # nobody answered inside 300s. Escalate. Do not send the request again
2) ;; # the id is not a ULID, or this store has never seen it
esacThe reply side needs nothing new — inside a hook the triggering id is already in the environment:
rite send "$RITE_CHANNEL" "on it" --reply-to "$RITE_MESSAGE_ID"Rules:
--reply-tonarrows.--mentionsand-cname places a message might turn up, so they combine with OR.--reply-tonames one question, and--from,-c, and-Lonly remove candidate answers from it. A wait that ended on an unrelated message would report an answer nobody gave.- With no
-c, every channel is watched, so a reply in a DM still counts. - Your own reply to your own message is not an acknowledgment.
- A reply that landed before the wait started is still reported. There is no
race between
rite sendandrite wait. - An id this store has never seen exits
2immediately instead of blocking. A mistyped id that merely timed out is how a requester concludes it was ignored. Use--allow-missing-parentwhen the parent is still syncing in from another machine.
Hooks let you trigger shell commands when messages arrive on channels. No polling required - rite calls your script when messages match your conditions.
# Fire when an agent is @mentioned on a channel
rite hooks add --channel general --mention reviewer --cwd . -- ./scripts/review.sh
# Fire when nobody holds a claim (i.e. that agent isn't already running)
rite hooks add --channel deploy --claim "agent://deployer" --ttl 600 --cwd . -- ./scripts/deploy.sh
# List all hooks
rite hooks list
# Test a hook without executing
rite hooks test <hook-id>
# Remove a hook
rite hooks remove <hook-id>--lease gives a hook at most one live spawn per channel, and batches
whatever arrives while that spawn is running:
rite hooks add --channel rite --mention rite-dev \
--lease --claim-owner rite-dev --cwd . -- vessel spawn rite-dev- The lease is an ordinary claim on a rite-owned pattern,
spawn://<hook-id>/<channel>, sorite claims listshows exactly who is holding a channel andrite hooks listshows how much is queued behind it. - Triggers that arrive while the lease is held are queued, deduplicated
(same sender + same body collapses), and handed to the next spawn through
RITE_BATCH_MESSAGE_IDS(comma-separated, oldest first, triggering message last) andRITE_BATCH_COUNT. Look each one up withrite messages get <id>. - A lease whose holder stops heartbeating (see
rite claims list,stale) is superseded: the next trigger takes its own lease and spawns. Nothing releases the dead holder's claim — it stays in the log, still active, still reported. A hook must never stop firing because an agent was killed. --lease-ttlbounds that recovery for a holder that never reported presence at all (default: the hook's claim TTL, else 3600s).- Set
--claim-ownerto the agent being spawned. Without it the lease is owned by whoever sent the triggering message, so presence tracks the sender rather than the spawn and only the TTL can clear a stuck lease.
--cooldown is deprecated. A wall clock cannot tell "still running" from
"finished early": too short double-spawns, too long silently drops everything
that arrived inside the window. It still works exactly as before for every
hook that has no lease, and is ignored on hooks that have one.
Subscriptions let you opt-in to channels so you only see messages from channels you care about.
# Subscribe to a channel
rite subscriptions add myproject
# List your subscriptions
rite subscriptions list
# Unsubscribe
rite subscriptions remove myprojectSet presence and status messages for your agent.
# Set your status
rite statuses set "Working on API migration"
# List all agent statuses
rite statuses list
# Clear your status
rite statuses clearStream new messages in real-time without polling.
# Watch all channels
rite watch --all
# Watch a specific channel
rite watch --channel generalrite mentions follow is the machine-facing version: one process, one file
watcher, and only the messages that actually concern you. It replaces "one
watcher per channel plus a filter", so its cost tracks how often you are
mentioned rather than total channel count and traffic.
# JSONL on stdout — one JSON object per line, flushed as produced.
# Mentions from every channel, plus your own DMs.
rite mentions follow --agent my-agent --format json
# Mentions only — suppress your DMs
rite mentions follow --agent my-agent --format json --no-dms
# Only review traffic
rite mentions follow --agent my-agent --format json -L reviewEach record carries a route discriminator (mention or dm) explaining why
it was forwarded, and a reply_target to pass straight back to rite send:
{"route":"mention","channel":"rite","reply_target":"rite","message":{"id":"01ARZ...","agent":"other-agent","body":"@my-agent can you look?","...":"..."}}Existing channels are seeded at their current end of file, so startup does not
replay history; channels created afterwards are read from the beginning, so a
channel whose first message is the mention is not missed. Your own DMs always
come through (that is what --no-dms is for), and DMs you are not a party to
are never forwarded, even if they mention you.
#general- Cross-project coordination, announcements#project-name- Project-specific updates (e.g.,#myapp,#backend)#project-topic- Focused discussion (e.g.,#myapp-api,#backend-auth)@agent-name- Direct messages
Channel names: lowercase alphanumeric with hyphens.
All data stored in ~/.local/share/rite/ (global, shared across projects):
~/.local/share/rite/
├── channels/
│ ├── general.jsonl
│ └── myproject.jsonl
├── claims.jsonl
├── state.json
└── index.sqlite
channels/*.jsonl- Message logs (append-only JSONL)claims.jsonl- File claims with absolute pathsstate.json- Per-agent read cursorsindex.sqlite- Full-text search index
Use rite agentsmd init to add rite instructions to your project's AGENTS.md:
rite agentsmd init # Auto-detect and update AGENTS.md
rite agentsmd init --file CLAUDE.md # Specify file
rite agentsmd show # Preview what would be addedOr manually add the output of rite agentsmd show to your agent instructions file.
"No agent identity set"
# Set identity for the session
export RITE_AGENT=$(rite generate-name)
# Or use a consistent name
export RITE_AGENT=my-agentPermission denied on data directory
# Check and fix permissions
ls -la ~/.local/share/rite
chmod 700 ~/.local/share/riteClaim conflicts
# See who has claims
rite claims list
# Ask the other agent to release, or wait
rite send @other-agent "Can you release src/api/**?"
rite wait -c @other-agent -t 60Search not finding messages
# Rebuild the search index
rm ~/.local/share/rite/index.sqlite
rite search "test" # Triggers rebuild# Full environment check
rite doctor
# Machine-readable diagnostics
rite --format json doctor
rite --format toon doctor
# Quarantine torn or junk log lines (dry run, then apply)
rite doctor --repair
rite doctor --repair --yes