A local multi-agent dispatcher wrapping Claude Code: Orchestrator → Workers → Heterologous Validator. Push notifications, behavioral contracts, a WebUI, and a stuck-session watchdog.
- Orchestrator (CC Opus) plans a task, writes a behavioral
contract.md, and dispatches Workers. - Code Workers (CC Opus) and RW Workers (CC Sonnet) produce artifacts in
workspace/<sid>/outputs/. Code workers serialize via a write lock; RW workers can parallelize. - Validator (Gemini 2.5 Pro, via
google-genaiSDK) sees ONLY the contract + worker self-reports + outputs file listing + test result files. It never sees source code. Writesverdict.md(PASSorFAIL). - WebUI (FastAPI + HTMX) on
http://127.0.0.1:7777shows live status, events, and artifacts. - Notify hooks push to Mac (terminal-notifier) and phone (ntfy.sh) on
Stop,Notification,SubagentStop, watchdog stuck, and dispatch-start. - Watchdog flags any session whose workspace has been idle longer than
WATCHDOG_TIMEOUT_SEC.
Dependencies (one-time):
brew install terminal-notifier tmux jq python@3.13
~/.claude-dispatch/.venv/bin/pip install fastapi uvicorn jinja2 \
google-genai python-dotenvclaude CLI must already be installed and authenticated.
Copy and edit .env:
cp ~/.claude-dispatch/.env.example ~/.claude-dispatch/.env
$EDITOR ~/.claude-dispatch/.envFill in:
GEMINI_API_KEY— get one at https://aistudio.google.com/apikey (starts withAIza...)NTFY_TOPIC— a long random string (e.g.openssl rand -hex 16); subscribe to it in the ntfy mobile app
DISPATCH_HOME, WEBUI_PORT, WATCHDOG_TIMEOUT_SEC, and NTFY_SERVER come with sane defaults.
Wire hooks into Claude Code's user settings (idempotent):
jq -s '.[0] * .[1]' ~/.claude/settings.json ~/.claude-dispatch/settings.json \
> /tmp/merged.json && mv /tmp/merged.json ~/.claude/settings.jsonWire the global behavioral contract:
ln -s ~/.claude-dispatch/CLAUDE.md ~/.claude/CLAUDE.md~/.claude-dispatch/bin/dispatch "create a python script that prints fibonacci, plus a README"You'll get a "🚀 Dispatch started" push. The Orchestrator session lives in a tmux session named dispatch-<sid>.
- WebUI: http://127.0.0.1:7777 — sessions list and per-session detail (2s HTMX polling). Auto-starts on first
dispatchcall. - Attach to a session:
tmux attach -t dispatch-<sid>— see the Orchestrator's chat thread live. - Files:
~/.claude-dispatch/workspace/<sid>/containstask.md,plan.md,contract.md,verdict.md,events.jsonl, and theoutputs/deliverables.
~/.claude-dispatch/bin/dispatch-stop <sid>Writes a session_end event and kills the tmux session.
Optional background daemon — alerts when a session's workspace has been idle longer than WATCHDOG_TIMEOUT_SEC:
nohup ~/.claude-dispatch/.venv/bin/python3 \
~/.claude-dispatch/watchdog/watchdog.py \
> /tmp/dispatch-watchdog.log 2>&1 &Logs to /tmp/dispatch-watchdog.log.
End-to-end check (runs ~5–10 min, costs ~$0.50 in API tokens):
bash ~/.claude-dispatch/bin/smoke-test.shDispatches a mathx + pytest task, polls for verdict.md, asserts PASS, sends a "🎉 Smoke test passed" notification.
No Mac banner notifications. macOS notification permission is required: open System Settings → Notifications → terminal-notifier, enable "Allow Notifications", set style to Banners or Alerts.
No phone push. Make sure you've installed the ntfy app (iOS / Android / F-Droid), added a subscription, and used the exact NTFY_TOPIC from .env. Anyone with this topic can read your notifications, so keep it long and random.
Sessions never finish (sit at "running" forever). Attach with tmux attach -t dispatch-<sid> and look at the Orchestrator's last message. If the watchdog is running you'll get a 🔴 Stuck push after WATCHDOG_TIMEOUT_SEC. Kill with dispatch-stop <sid>.
Write-lock seems stuck. Code workers serialize via a directory lock at workspace/<sid>/.write-lock/. If a code worker is killed mid-run the lock dir may persist:
rmdir ~/.claude-dispatch/workspace/<sid>/.write-lockWebUI won't start. Check the log: tail /tmp/dispatch-webui.log. Common cause: port 7777 already in use by another process (lsof -i :7777). Set a different WEBUI_PORT in .env.
Orchestrator stalls on a "trust this folder?" dialog. bin/dispatch pre-registers each workspace in ~/.claude.json. If you see this prompt it means jq failed during launch — check that jq is installed.
~/.claude-dispatch/
├── BUILD_SPEC.md — the build specification (for reference)
├── CLAUDE.md — Karpathy 4 behavioral rules (symlinked to ~/.claude/)
├── settings.json — hooks template (merged into ~/.claude/settings.json)
├── .env — your secrets (gitignored)
├── agents/
│ ├── orchestrator.md — Orchestrator system prompt
│ ├── worker-code.md — Code Worker system prompt
│ └── worker-rw.md — RW Worker system prompt
├── bin/
│ ├── dispatch — launch an Orchestrator
│ ├── dispatch-stop — kill a session
│ ├── spawn-worker.sh — called by Orchestrator
│ └── smoke-test.sh — end-to-end test
├── hooks/ — notify.sh + on-stop/on-notification/on-subagent-stop
├── validator/ — validate.py + prompt.md (Gemini, heterologous)
├── watchdog/ — watchdog.py
├── webui/ — FastAPI + HTMX observatory
├── .venv/ — Python 3.13 venv (gitignored)
└── workspace/<sid>/ — per-dispatch state (gitignored)