EJClaw
Tribunal multi-agent AI assistant (Owner + Reviewer + Arbiter) over Discord with autonomous paired review and Mixture of Agents.
Originally derived from qwibitai/nanoclaw, now independently maintained as EJClaw. Prompt design inspired by Q00/ouroboros and garrytan/gstack, adapted for EJClaw's Discord and dual-service workflow. Tribunal arbiter system inspired by multi-agent consensus architectures.
Overview
A single unified service (ejclaw) runs a Tribunal of three roles while
managing three Discord bots in one process:
| Role | Purpose |
|---|---|
| Owner | Handles user requests, writes code |
| Reviewer | Critically reviews owner's work, verifies design direction |
| Arbiter | On-demand deadlock breaker between owner and reviewer |
The identity layer is role-fixed:
- Owner bot — Handles the owner turn output slot.
- Reviewer bot — Handles the reviewer turn output slot.
- Arbiter bot — Handles the arbiter turn output slot.
Each role's agent type and model are independently configurable via .env
(OWNER_AGENT_TYPE, REVIEWER_AGENT_TYPE, ARBITER_AGENT_TYPE, *_MODEL).
Three Discord bots provide the identity layer — which bot speaks is determined
by the active role, not hardcoded.
Room Assignment Model
Per-room routing now uses an explicit assignment model:
room_settingsis the room-level source of truth (SSOT)- Each room stores:
room_mode:singleortribunalowner_agent_type:codexorclaude-code
- Public room assignment uses
assign_room - Legacy
register_grouppublic interfaces were removed registered_groupsremains as a materialized capability/read-model layer and legacy fallback, not the authoritative room configuration
Operationally:
single→ one owner bottribunal→ per-room owner + globally configured reviewer + optional arbiter
This means tribunal is no longer inferred from “two bots registered on one room”; it is an explicit room setting.
Tribunal Flow
User message
→ Owner responds (implementation, answer, etc.)
→ Reviewer auto-triggered (critical review, design check)
→ Verdict:
DONE → Owner finalizes → Task completed → @user ✅
DONE_WITH_CONCERNS → Owner addresses feedback → loop
BLOCKED/NEEDS_CONTEXT
├─ Arbiter enabled → Arbiter judges → PROCEED/REVISE/RESET/ESCALATE
└─ Arbiter disabled → Escalate to user → @user ⚠️
→ Owner BLOCKED/NEEDS_CONTEXT → Arbiter (same path as reviewer)
→ Deadlock (2+ round trips without progress)
→ Arbiter summoned → binding verdict → loop resumes
Mixture of Agents (MoA)
When enabled, the arbiter collects opinions from external models (Kimi, GLM, etc.) before rendering its verdict:
Deadlock detected → MoA reference queries (Kimi + GLM, parallel)
→ Opinions injected into arbiter's prompt
→ SDK arbiter (subscription-based) aggregates all perspectives
→ Final verdict: PROCEED / REVISE / RESET / ESCALATE
No extra SDK processes. External references use lightweight API calls (Anthropic-compatible).
Features
- Tribunal 3-agent system — Owner/reviewer/arbiter with on-demand deadlock resolution
- Discord-independent communication — Agent-to-agent data flows directly via DB, Discord is display-only
- Mixture of Agents — External model opinions (Kimi, GLM) enrich arbiter verdicts
- Per-role model selection —
OWNER_MODEL,REVIEWER_MODEL,ARBITER_MODEL+ effort + fallback toggle - Container-isolated reviewer — Persistent Docker container with read-only source mount
- Global failover — Account-level Claude failure → all channels switch to codex, auto-recovers
- Post-approval change detection — Re-triggers review if owner modifies code after approval
- Auto user notification — @mention on task completion (✅ done, ⚠️ escalated)
- Loop protection — Deadlock threshold, merge_ready oscillation guard, arbiter re-invocation limit
- Voice transcription — Groq Whisper (primary) / OpenAI Whisper (fallback)
- Token rotation — Multi-account Claude/Codex rotation on rate limits
- Kimi usage dashboard — Coding plan 5h/7d usage displayed alongside Claude/Codex
- MCP integration — Memento (persistent memory) + EJClaw host tools
- Session persistence — Separate sessions per role (owner/reviewer/arbiter)
- Scheduled tasks — Cron/interval/once via MCP tool
- Mid-turn steering — Inject follow-up messages while agent is working
- Bun runtime — Native SQLite (bun:sqlite), fast startup, no native addon builds
Architecture
Discord ──► SQLite (WAL) ──► GroupQueue ──┬──► Owner (host process)
│ │
│ ▼ (auto-trigger)
├──► Reviewer (Docker container, :ro mount)
│ │
│ Verdict routing
│ ├─ DONE → change detection → finalize or re-review
│ ├─ BLOCKED → Arbiter (if enabled) or User
│ └─ Feedback → Owner (loop)
│
├──► Arbiter (on-demand, fresh session each time)
│ │
│ ┌───┴─── MoA (if enabled) ───┐
│ │ Kimi API ──► opinion │
│ │ GLM API ──► opinion │
│ │ → injected into prompt │
│ └────────────────────────────┘
│ │
│ PROCEED/REVISE/RESET/ESCALATE
│
IPC polling ◄── follow-up messages
│
┌────────── Router ──────────┐
▼ ▼
paired_turn_outputs Discord (display only)
(agent ↔ agent data) (user observation, @mention)
Setup
Prerequisites
- Linux (Ubuntu 22.04+) or macOS
- Bun 1.3+
- Docker (required for reviewer container isolation)
- Claude Code CLI
- Codex CLI (
npm install -g @openai/codex) - Discord bot tokens (3: owner, reviewer, arbiter)
Install
git clone https://github.com/phj1081/EJClaw.git
cd EJClaw
bun install
bun run build:runners
bun run build
bun run build:container # Build reviewer Docker image
Documentation
- Architecture — Data flow, room model, container isolation, key files
- Configuration — Full
.envreference, debugging paths
Environment
All configuration in a single .env file:
# Discord bots (canonical role-fixed names)
DISCORD_OWNER_BOT_TOKEN= # Owner bot
DISCORD_REVIEWER_BOT_TOKEN= # Reviewer bot
DISCORD_ARBITER_BOT_TOKEN= # Arbiter bot
# Old service-based token names are no longer accepted.
# Rename existing values to the canonical role-based keys above.
# Agent types
OWNER_AGENT_TYPE=codex # codex | claude-code
REVIEWER_AGENT_TYPE=claude-code # claude-code | codex
ARBITER_AGENT_TYPE=codex # codex | claude-code (optional, enables 3rd agent)
# Per-role model overrides
OWNER_MODEL=gpt-5.4
REVIEWER_MODEL=claude-opus-4-6
ARBITER_MODEL=gpt-5.4
# API keys
CLAUDE_CODE_OAUTH_TOKEN= # Claude Code OAuth token
CLAUDE_CODE_OAUTH_TOKENS= # Comma-separated for multi-account rotation
GROQ_API_KEY= # Voice transcription (Groq Whisper)
# Mixture of Agents (MoA)
MOA_ENABLED=true
MOA_REF_MODELS=kimi,glm
MOA_KIMI_MODEL=kimi-k2.5
MOA_KIMI_BASE_URL=https://api.kimi.com/coding
MOA_KIMI_API_KEY=sk-kimi-xxx
MOA_KIMI_API_FORMAT=anthropic
MOA_GLM_MODEL=glm-5.1
MOA_GLM_BASE_URL=https://open.bigmodel.cn/api/anthropic
MOA_GLM_API_KEY=xxx
MOA_GLM_API_FORMAT=anthropic
Deploy
bun run deploy
Development
bun run build # Build main project
bun run build:runners # Install + build both runners
bun run build:container # Rebuild reviewer Docker image
bun run dev # Dev mode with hot reload
bun test # Run tests
License
MIT — Originally derived from qwibitai/nanoclaw