wrightward¶
Multi-agent coordination for Claude Code. When two or more sessions work in the same repo, wrightward blocks conflicting writes, injects awareness context, and gives sessions a peer-to-peer message bus (eight MCP tools) to hand off tasks, watch files, and wake each other. Ships with an optional Discord bridge.
Version: 3.10.0 · Source · README
Install¶
Requires Node.js ≥ 18 and a git repo. Hooks activate automatically. State lives at <repo-root>/.claude/collab/ (auto-gitignored).
Recommended permissions¶
Add to your global ~/.claude/settings.json to skip consent dialogs:
{
"permissions": {
"allow": [
"Skill(wrightward:collab-context)",
"Skill(wrightward:collab-done)",
"Skill(wrightward:collab-release)",
"mcp__wrightward-bus__*"
]
}
}
The mcp__wrightward-bus__* entry auto-allows every wrightward MCP tool — important so wake-up pings don't stall on a permission prompt.
File coordination¶
Default-on. No setup. Every Edit/Write is auto-tracked.
- Auto-tracked files are held for 2 minutes from last touch.
- Declared files (via
/wrightward:collab-context) are held for 15 minutes, auto-extended if the agent edits near the deadline. - Idle reminder fires after 5 minutes of no touches, suggesting release.
Guard behavior¶
Before every tool call:
- Read/Glob/Grep on another agent's file → awareness context injected (who owns it, what they're doing).
- Write to another agent's file → blocked; the agent sees who owns it.
- Write to an unrelated file → proceeds with awareness of other active agents.
- Solo agent → everything proceeds silently.
Context injection is deduplicated — the same summary appears once per change.
Collab state is protected¶
Edit/Write on .claude/collab/* is hard-blocked by the guard hook. Bash is not intercepted — the block message and every collab skill tell the agent never to escalate to shell.
Slash commands¶
All live under /wrightward:<name>.
| Command | Purpose |
|---|---|
/wrightward:help |
Rulebook — tool reference, coordination rules, Discord routing, event types, etiquette. |
/wrightward:collab-context |
Declare or update task + claimed files. JSON payload with task, files (+ create / ~ modify / - delete), functions, status. |
/wrightward:collab-release |
Release specific files. JSON payload with files array. |
/wrightward:collab-done |
Release everything and exit coordination. |
/wrightward:inbox |
List pending urgent events (rarely needed — auto-injected). |
/wrightward:ack |
Acknowledge a handoff with accepted / rejected / dismissed. |
/wrightward:handoff |
Hand a task to another agent and release files atomically. |
/wrightward:watch |
Register interest in a file — get notified when it frees up. |
/wrightward:config-init |
Write .claude/wrightward.json with every default populated. Pass --force to overwrite. |
Message bus (v3.0)¶
Sessions hand off work, watch files, and notify each other through .claude/collab/bus.jsonl (append-only, length-bounded, self-compacting). Urgent events inject as additionalContext on the next tool call.
MCP tools¶
| Tool | Required | Optional | Purpose |
|---|---|---|---|
wrightward_list_inbox |
— | limit, types, mark_delivered (default true) |
List urgent events targeted at this session. Advances the bookmark. |
wrightward_ack |
id |
decision ("accepted" | "rejected" | "dismissed") |
Acknowledge a handoff. Routes the ack at the sender so they see it on their next tool call and in their Discord thread. |
wrightward_send_note |
body |
to (handle or "all"), kind ("note" | "finding" | "decision", default "note"), files |
Log an observability entry. note is quiet; finding/decision are urgent and broadcast. |
wrightward_send_handoff |
to (peer handle e.g. "bob-42"), task_ref, next_action |
files_unlocked |
Hand work to another session by handle. Atomically releases the listed files and emits file_freed to watchers. |
wrightward_watch_file |
file |
— | Register interest. You get a file_freed event when the owner releases it. |
wrightward_bus_status |
— | — | Diagnostic — pending urgent count, recent timestamp, bound session ID, bridge status. |
wrightward_send_message |
body, audience |
— | Send a message via Discord. audience = "user" (reply into the sender's own thread), "all" (Discord broadcast + every agent's inbox), or a peer handle like "bob-42" (that agent's thread + inbox). Requires the Discord bridge to be running for Discord delivery. |
wrightward_whoami |
— | — | Return your own agent handle, session ID, and registration time. Handles are deterministic per-session; useful after compaction. |
Event types (15)¶
Nine urgent (auto-inject on next tool call, capped by BUS_URGENT_INJECTION_CAP); six non-urgent (persisted, not auto-surfaced).
Urgent (9): handoff, file_freed, user_message, blocker, delivery_failed, agent_message, ack, finding, decision.
Non-urgent (6): note, interest, session_started, session_ended, context_updated, rate_limited.
Routing¶
tois a session ID,"all", or an array of session IDs.from === tonever matches — no echo.ambiguous_mentionflag signals a short-ID collision resolved to"all".
Channel push (v3.1, research preview)¶
Adds a notifications/claude/channel wake-up ping so idle sessions notice new events between turns. Requires Claude Code ≥ 2.1.80. Gated behind Anthropic's allowlist — until wrightward is approved, use the server: workaround.
Enable¶
-
Add the server to your user-level
~/.mcp.json: -
Launch Claude Code with the dev flag:
Expected banner: "Listening for channel messages from: server:wrightward-bus".
Wait ~10 seconds between concurrent agent launches¶
When spinning up multiple CLI agents in the same repo, wait about 10 seconds between each claude ... command. The MCP server binds to its session via a ticket file written by the SessionStart hook; on Windows (and other setups where process.ppid doesn't match the Claude Code process directly), the fallback scanner refuses to bind across more than one unclaimed ticket in its 10-second freshness window. Launching 2+ agents back-to-back leaves them all unbound — channel wake-ups silently stop until the session restarts. Spacing launches by 10s or more avoids this.
IDE extensions are not supported¶
Channels only work when Claude Code is launched from a plain terminal. The VS Code and Cursor extensions do not deliver notifications/claude/channel wake-up pings — the Path 2 doorbell is silently dropped regardless of claudeCode.claudeProcessWrapper or dev-flag configuration. Path 1 still works inside the IDEs (urgent events inject on the session's next tool call), so you won't lose delivery — just between-turn wake-ups. Launch claude from a terminal if you need the doorbell.
Discord bridge (v3.2)¶
An opt-in subprocess that mirrors bus events to Discord. REST-only (no gateway), so it coexists with the stock discord@claude-plugins-official plugin on the same bot token.
When enabled:
- Creates one forum thread per agent named
<task> (<handle>)(e.g.refactor auth (bob-42)) and posts per-session events there. Handles are deterministic per-session — the same UUID always derives the same<name>-<number>handle. - Mirrors
session_started/session_ended/ broadcast handoffs /user_messagetargeted at"all"into a shared broadcast text channel. - Watches the broadcast channel and every live agent thread for inbound messages, routing them back into
bus.jsonlasuser_messageevents. - Renames a thread when
/wrightward:collab-contextupdates the session's task.
Setup¶
-
Create a Discord application and bot at https://discord.com/developers/applications.
- Click New Application, name it, then go to the Bot tab and click Add Bot.
- Under Token, click Reset Token and copy it (shown once). Keep it secret.
- Turn on Message Content Intent. Still on the Bot tab, scroll to Privileged Gateway Intents and toggle Message Content Intent on. Without it, Discord strips message bodies —
@agent-<id>mentions can't resolve, and thread replies arrive empty. Enable it.
-
Invite the bot via OAuth2 → URL Generator.
- Scopes:
bot. - Permissions:
View Channels,Send Messages,Send Messages in Threads,Manage Threads,Read Message History. - Open the generated URL and add the bot to a server you own.
- Scopes:
-
Create two channels in your Discord server:
- One forum channel — one thread per agent.
- One text channel — the shared broadcast feed.
-
Enable Developer Mode in Discord (User Settings → Advanced → Developer Mode). Right-click to Copy ID. You'll need:
- The forum channel ID (→
FORUM_CHANNEL_ID). - The broadcast text channel ID (→
BROADCAST_CHANNEL_ID). - Your Discord user ID (→
ALLOWED_SENDERS— right-click your name → Copy User ID).
- The forum channel ID (→
-
Install and provide the bot token.
/plugin install wrightward@Joys-Dawn/toolwright. On first run, Claude Code prompts for thediscord_bot_token— paste it and you're done. The token is stored in your OS keychain (declaredsensitive: trueinplugin.json) and passed to the bridge asDISCORD_BOT_TOKEN.Alternatively, set
DISCORD_BOT_TOKENin the shell that launchesclaude: -
Create
.claude/wrightward.jsonin your project root:{ "discord": { "ENABLED": true, "FORUM_CHANNEL_ID": "1234567890", "BROADCAST_CHANNEL_ID": "1234567891", "ALLOWED_SENDERS": ["your-discord-user-id"] } }ALLOWED_SENDERS: []runs the bridge send-only. -
Start a Claude session. The first session spawns the bridge under a single-owner lockfile at
.claude/collab/bridge/bridge.lock. Other sessions in the same repo share it — exactly one bridge per repo.
Verify with wrightward_bus_status — the bridge sub-object should show running: true, last_error: null, and owner_session_id matching your session. Tail .claude/collab/bridge/bridge.log for diagnostics.
Inbound routing¶
All gated on ALLOWED_SENDERS:
- Reply in an agent's forum thread → delivered to that thread's session without an
@mention. @agent-<handle>in broadcast or thread → delivered to the mentioned session(s). Both full handle (@agent-bob-42) and name-only (@agent-bob) resolve; name-only matches a single agent by name, otherwise broadcasts withambiguous_mention: true. Fan-out works: a thread reply with extra@agent-<handle>mentions goes to the union of the thread owner and mentioned sessions.@agent-all→ every registered agent.ambiguous_mentionstaysfalsefor explicit all-broadcasts.
Mirror policy defaults¶
User overrides merge on top. Demote to silent via mirrorPolicy if noisy.
| Event type | Destination |
|---|---|
user_message, handoff, blocker, agent_message |
Recipient's thread. Promotes to broadcast when sent to "all". agent_message with audience="user" posts into the sender's own thread (not broadcast). |
file_freed |
Recipient's thread (targeted only); silent when broadcast. |
session_started, session_ended |
Broadcast channel. |
note, finding, decision |
Target thread when sent to a sessionId; broadcast channel when sent to "all". |
ack |
Original handoff sender's thread. |
context_updated |
Renames the sender's thread to match the new task. |
interest, delivery_failed, rate_limited |
Never mirrored (hard rail — can't be elevated). |
Security model¶
ALLOWED_SENDERSgates on Discord user ID, not channel membership. Access to the broadcast channel alone doesn't grant inbound rights.- Bot tokens,
Bot <token>headers, and Discord webhook URLs are scrubbed from everybridge.logwrite and every inbound body before it reachesbus.jsonl. - Inbound content clamped at 4000 bytes on a UTF-8 boundary.
- Outbound messages exceeding Discord's 2000-byte cap auto-split into ordered posts with
(n/N)continuation markers, balanced code fences across chunks, and UTF-8-safe cuts — no silent truncation. - Discord-originated events use the reserved
systemsender withmeta.source: "discord". A loop guard prevents re-mirroring back to Discord. - Local operation keeps flowing if Discord is down — the bridge retries in the background.
Hooks¶
| Hook | Event | What it does |
|---|---|---|
register.js |
SessionStart |
Registers the agent in .claude/collab/agents.json. Emits session_started on source=startup\|resume only. |
heartbeat.js |
PostToolUse (all tools) |
Updates heartbeat, auto-tracks files, scavenges stale sessions, fires idle reminders. |
guard.js |
PreToolUse (Edit\|Write\|Read\|Glob\|Grep) |
Blocks conflicting writes; injects awareness context. |
bash-allow.js |
PreToolUse (Bash) |
Auto-approves wrightward's own script invocations (workaround for claude-code#11932). |
plan-exit.js |
PostToolUse (ExitPlanMode) |
Reminds the agent to declare files — only when other agents are active. |
cleanup.js |
SessionEnd |
Deregisters, releases claims, emits session_ended. |
Config¶
.claude/wrightward.json (all fields optional). See wrightward.example.json.
Run /wrightward:config-init to drop the full default config into your repo — every key populated so you can edit any knob in place. Add --force to overwrite an existing file; delete the file to fall back to built-in defaults.
Base coordination¶
| Key | Default | Description |
|---|---|---|
ENABLED |
true |
Master switch. false exits all hooks immediately. |
PLANNED_FILE_TIMEOUT_MIN |
15 | How long declared files are held. |
PLANNED_FILE_GRACE_MIN |
2 | Extends the timeout when the file is touched near expiry. |
AUTO_TRACKED_FILE_TIMEOUT_MIN |
2 | How long auto-tracked files are held (from last touch). |
REMINDER_IDLE_MIN |
5 | Idle threshold for the release reminder. |
INACTIVE_THRESHOLD_MIN |
6 | Stale-session threshold. |
SESSION_HARD_SCAVENGE_MIN |
60 | Hard cleanup for dead sessions. |
AUTO_TRACK |
true |
Auto-create a context when none has been declared. |
Bus¶
| Key | Default | Description |
|---|---|---|
BUS_ENABLED |
true |
false skips the MCP server entirely. |
BUS_RETENTION_DAYS |
7 | Drop events older than this from bus.jsonl. |
BUS_RETENTION_MAX_EVENTS |
10000 | Hard cap on retained events. |
BUS_HANDOFF_TTL_MIN |
30 | Handoffs expire if never acked. |
BUS_INTEREST_TTL_MIN |
60 | TTL on file-watch registrations. |
BUS_URGENT_INJECTION_CAP |
5 | Max urgent events auto-injected per tool call. Overflow points to /wrightward:inbox. |
Discord¶
| Key | Default | Description |
|---|---|---|
discord.ENABLED |
false |
Master switch for the bridge. |
discord.FORUM_CHANNEL_ID |
— | Forum channel for per-agent threads. |
discord.BROADCAST_CHANNEL_ID |
— | Text channel for announcements and inbound mentions. |
discord.ALLOWED_SENDERS |
[] |
Discord user IDs permitted to route inbound messages. |
discord.POLL_INTERVAL_MS |
3000 | How often to poll the broadcast channel and each active thread. |
discord.THREAD_RENAME_ON_CONTEXT_UPDATE |
true |
Whether /wrightward:collab-context task changes rename the thread. |
discord.BOT_USER_AGENT |
DiscordBot (https://github.com/Joys-Dawn/toolwright, 3.10.0) |
Override only with a reason; must start with the literal DiscordBot to avoid Cloudflare blocking. |
discord.mirrorPolicy |
see above | Per-event-type override. |
Disable in a repo¶
All hooks exit immediately.
State layout¶
.claude/collab/:
context/,context-hash/— per-session task/file claimsmcp/— MCP server binding ticketsbus.jsonl— append-only event logbus-delivered/— per-session delivery bookmarksbus-index/— derived indices (Discord thread map, etc.)bridge/— Discord lockfile, log, circuit breaker, last-polled markers
Security¶
- No network I/O by default. Outbound traffic happens only when the Discord bridge is enabled, in which case the bridge talks to
discord.com/api/v10via REST. - Channel notifications carry no payload. The doorbell emits a fixed short string; event content always flows through hook-injected
additionalContext. - Bus messages originate only from co-located sessions — no external sender.
- Edit/Write to collab state are hard-blocked. Bash isn't intercepted; the agent is prompted never to escalate to shell.
- Advisory file locking on every bus mutation, with stale-lock detection.
- No telemetry.