A 4-story walkthrough of the multi-agent task board. Each story is self-contained; run them in order on a fresh install or jump to whichever matches your use case.
Pre-req: Phase 1 board shipped (./vodou-core board --help lists 22 verbs). Run ./do board migrate --init once if board.db doesn't exist yet.
You have a one-line idea: "research ICP funding 2024-2026 NA angle." Three commands to go from idea → finished research brief.
./do board create "research ICP funding 2024-2026 NA" --triageThe --triage flag puts it in the Triage column (raw 1-liner stage). The librarian skill — board-triage-specifier — picks it up via the scheduled-task pattern and fleshes it out with goal, approach, acceptance criteria from your memory + USER.md context.
./do board specify --all # runs the librarian on every triage card
./do board assign t_<id> researcher # pick the assignee
./do board list --status ready # see what's queuedWithin 30 seconds the embedded dispatcher (running inside the gateway) claims the task and spawns a claude -p worker with the worker_context pre-loaded — memory chunks, parent handoffs, role history, the BOARD_GUIDANCE block.
When the worker calls board_complete(summary, metadata), the dispatcher writes a task_handoff memory chunk so future similar tasks auto-load this run's summary. Compounding institutional knowledge — Hermes can't do this.
./do board show t_<id>
# Run History section shows the worker's full summary + token cost + durationYou have a 1-paragraph plan. You want it sliced into 8 child tasks across 4 subagents.
./do board create "Ship v0.5.78 launch announcement" --triage --body "$(cat plan.md)"
./do board orchestrate <id> # board-orchestrator skill runs board_assignees() firstThe orchestrator skill follows its anti-temptation rules (decompose, don't execute). Step 0 calls board_assignees() so it only assigns to subagents that actually exist. Then board_create(...) + board_link(parent, child) for each leaf node. Cycles refused at the DB layer (DFS check in add_link).
./do board show <parent_id> # shows the spec + the 8 children chips
./do board dispatch # one tick: ready→running for parentless childrenThe dispatcher's per-tick logic (src/board/dispatcher.rs):
- Reclaims stale claims (TTL expired)
- Detects crashed PIDs (zombie-aware via /proc on Linux, kill(0) on macOS)
- Enforces max_runtime (SIGTERM → 5s → SIGKILL on process group)
- Promotes
todo → readywhen all parents aredone - Atomic CAS claim (BEGIN IMMEDIATE + single UPDATE)
- Spawns worker with build_worker_context → claude CLI
- Circuit-breaks after N consecutive
spawn_failed - Health telemetry
You run 8 customer tenants. Each gets its own board (atm10-server, acme-corp, etc.). You want one dashboard.
(Phase 3 multi-board ships this UX. Phase 1 has the tenant_id field on every task and the schema is multi-board-ready via board_id — but the per-tenant board switcher in the dashboard is Phase 3 work.)
For Phase 1 you can already filter by tenant:
./do board list --tenant acme --json
curl -s http://127.0.0.1:8765/api/board?tenant=acme | jqYou go to bed at 11pm with 4 tasks in Triage. You want them specified, assigned, executed, and reported on by 7am.
Phase 1 ships the scheduled-task pattern via the existing scheduled_tasks table:
sqlite3 vodou-core.db "
INSERT INTO scheduled_tasks (name, schedule, schedule_type, payload_type, payload)
VALUES ('nightly-triage-sweep', '*/10 * * * *', 'cron', 'board_specify_all',
json_object('board', 'default'));
"(This integrates with the existing src/scheduler.rs::run_due_tasks loop. The Day-11 board_specify_all payload type lands in a future session; today, you can run ./do board specify --all manually as a one-shot.)
./do board notify-subscribe t_<id> channel:telegram:<chat_id>
./do board notify-subscribe t_<id> webhook:https://my-monitoring-endpoint.example.com
./do board notify-subscribe t_<id> inapp:principal:<your-principal-id>The embedded notifier (running every 5s inside the gateway) polls task_events, formats per-platform messages, and dispatches:
channel:slack:*→ standaloneslackMCP serverchannel:<other>:*→Vodou-channels::send_messageinapp:principal:*→ row ingateway_in_app_inbox(dashboard bell-icon)webhook:<url>→ POST event payload to URL
Failure counter per sub; auto-prune after 3 consecutive failures.
./do board list --status done # what shipped overnight
./do board stats --since 12h # cycle time + cost (Phase 3)
sqlite3 board.db "SELECT body FROM gateway_in_app_inbox ORDER BY id DESC LIMIT 5"You have a project (a Vodou-registered project, or any folder). You want Vodou to read the actual code, plan the next steps, and have workers implement them in that repo.
- Open the board → click 🧭 Plan.
- In Plan against:, pick your project (or Custom folder… and drop/paste
any path — e.g.
/Users/you/Desktop/myapp). - Type the goal: "What are the next things to build or wire up here? Give an ordered plan of concrete tasks, each naming real files/modules."
- Click Plan ↵.
The log shows reading project: <name> → inspecting <name> codebase… →
composing the plan. The planner actually reads the repo (README, manifest,
src/, PLAN docs) — so you get tasks like "Wire identify() into the canonical
id path (identity/musicbrainz.ts)", not generic advice. Takes a couple minutes;
you can navigate away and come back — the run keeps going and re-attaches.
Refine by typing a follow-up; start a different plan with + New plan.
- Commit the draft → the tasks land in the Plan column, each stamped
workspace = dir:<yourRepo>. - Commit the target repo first (
git add -A && git commit) — workers edit in place; this is your undo baseline. - Drag ONE card into Ready, then click Dispatch tick (or
./vodou-core board dispatch). The worker spawns inside your repo.
cd /path/to/your/repo
git status --short && git diff --stat # files the worker changed, live
npm test # verify (a good worker leaves it green)
git restore . # …or throw the changes awayThe task card's summary is a real handoff ("Changed: src/… ; 133 tests pass;
build clean"). Verified end-to-end: an MTVai plan → committed → dispatched → a
worker wired identify() across 4 files, all 133 tests green, tsc clean.
| Verb | Purpose |
|---|---|
migrate |
Apply pending board.db migrations |
create |
Create a task (with --parent/--status/--triage/--assignee/--priority/--workspace/--max-runtime/--skill/--model) |
list |
List tasks with filters (--status, --assignee, --tenant, --archived, --limit, --json) |
show |
Show one task in full (meta + body + runs + comments + events + parents/children) |
assign |
Set or clear assignee |
link / unlink |
Manage parent → child dependencies |
comment |
Append a comment (with optional --reply-to) |
complete |
Close task as done (with --summary + --metadata) |
block / unblock |
Block (with reason) or restore to ready |
archive |
Soft-delete (status → archived) |
heartbeat |
Bump last_heartbeat_at |
runs |
List run history |
dispatch |
Run one dispatcher tick (--dry-run, --max, --json) |
assignees |
List active subagents with in-flight counts |
search |
FTS5 search across title + body |
notify-subscribe / notify-list / notify-unsubscribe |
Manage channel subs |
notifier |
Run one notifier tick |
Every read verb supports --json. migrate, create, complete, runs, dispatch, assignees, search, notify-list, notifier all return clean machine-readable JSON for scripting.
The board is reachable from three places — all hitting the same board.db:
| Surface | Endpoint | Use case |
|---|---|---|
| CLI | ./do board <verb> |
scripts, terminal users, cron |
| REST | POST /api/board/tasks, GET /api/board, etc. (15 endpoints) |
external integrations (n8n, Zapier, GitHub Actions) |
| MCP | board_show / board_complete / etc. (14 tools, env-gated on VODOU_BOARD_TASK) |
LLM workers (claude CLI invoked by the dispatcher) |
| Dashboard | http://localhost:8765/#/board |
drag-drop UI, drawer, live event polling |
The dashboard polls /api/board/events?since=<last_id> every 3 seconds and refreshes on state-change events. WebSocket upgrade is Phase 2.
Phase 1 shipped parity. 5 of 7 Phase-2 cuts are live (see kanban-board.md for the full status table):
| Hermes | Kanban Board | Status |
|---|---|---|
| Worker fetches user context manually | Memory-injected worker_context.memory[] (dispatcher pre-runs hybrid FTS + embedding + rerank against memory.db) |
✅ §3.1 |
| Workers see no tools | Per-task .mcp.json — 8 of 11 curated Vodou MCP servers ✓ Connected when claude spawns |
✅ §3.2 |
| Text-only channel notifications | Interactive Slack Block Kit + Discord buttons routed through /api/board/channel-action |
✅ §3.4 |
| No workflow templates | board_templates with auto-advance on stage complete |
✅ §3.5 |
| Cost per call only | Per-task budget caps — USD/tokens/runtime, soft + hard, dispatcher terminates on BudgetExceeded |
✅ §3.6 |
| Approval gates "out of scope" | First-class governance — requires_approval_on gates, suspended→approved/denied state machine, full audit trail |
✅ §3.4 |
| No semantic Q&A | board ask — natural-language Q&A over board state (8 intent classifiers) |
✅ Day 11 |
| Single-host | Multi-host federation | ⏭️ Phase 4 |
| Worker logs (tail 100KB) | AI Replay panel — replay the worker's full thought chain inline | ⏭️ Phase 3 UX |
./scripts/smoke-board.sh # 8-stage end-to-end smoke (~2s)
VODOU_BOARD_REAL_SPAWN=1 ./scripts/smoke-board.sh # adds real claude spawn verification
cargo test --lib board:: # 48 Rust unit tests
cd MCP-servers/Vodou-Board && npm test # 32 vitest testsIf any test fails, check .vodou/system.log and BUILD-PHASE-1-CHECKLIST.md §6 gotcha list.