Files
hermes-agent/website/docs/user-guide/features/kanban.md
Teknium af8d43dbbb feat(kanban): core hardening — daemon, circuit breaker, crash detect, logs, notify, bulk, stats
Eliminates every 'known broken on day one' item in the core functionality
audit. The board is now self-driving (daemon, not cron), self-healing
(crash detection, spawn-failure circuit breaker), and self-reporting
(logs, stats, gateway notifications).

Dispatcher
  - New `hermes kanban daemon` long-lived loop with --interval, --max,
    --failure-limit, --pidfile, --verbose, signal-clean shutdown
    (SIGINT/SIGTERM via threading.Event). A kb.run_daemon() entry point
    lets tests drive it inline without subprocess.
  - `hermes kanban init` now prints the dispatcher setup hint so users
    don't leave the board off-by-default. Ships a systemd user unit at
    plugins/kanban/systemd/hermes-kanban-dispatcher.service.
  - Removed the old 'add this to cron' doc path. Cron runs agent
    prompts (LLM cost per tick) — unacceptable for a per-minute
    coordination loop.

Worker aliveness / safety
  - Spawn returns the child's PID; dispatcher stores it on the task row
    and calls detect_crashed_workers() every tick. If the PID is gone
    but the claim TTL hasn't expired, the task drops back to ready with
    a 'crashed' event. Host-local only — cross-host PIDs are ignored
    per the single-host design.
  - Spawn-failure circuit breaker: after N consecutive spawn_failed
    events on the same task (default 5), the dispatcher auto-blocks
    with the last error as the reason. Success resets the counter.
    Workspace-resolution failures count against the same budget.
  - Log rotation: _rotate_worker_log trims at 2 MiB, keeps one
    generation (.log.1), bounds per-task disk usage at ~4 MiB.

Idempotency / dedup
  - create_task(idempotency_key=...) returns the existing non-archived
    task id for retried webhooks. --idempotency-key on the CLI, json
    body field on the dashboard plugin. Archived tasks don't block a
    fresh create with the same key.

CLI surface
  - Bulk verbs: complete, unblock, archive accept multiple ids;
    block accepts --ids for sibling blocks with the same reason.
  - New verbs: daemon, watch (live event tail filtered by
    assignee/tenant/kinds), stats, log, notify-subscribe,
    notify-list, notify-unsubscribe.
  - dispatch gains --failure-limit + crashed/auto_blocked columns in
    JSON output and human-readable output.
  - gc accepts --event-retention-days / --log-retention-days; prunes
    task_events for terminal tasks and old log files.

Gateway integration
  - New GatewayRunner._kanban_notifier_watcher: polls
    kanban_notify_subs every 5s, pushes ✔/⏸/✖ messages to subscribed
    chats for completed/blocked/spawn_auto_blocked/crashed events.
    Cursor-advanced per-sub; auto-removed when the task reaches
    done/archived. Runs alongside the session expiry and platform
    reconnect watchers — SQLite work in asyncio.to_thread so the
    event loop never blocks.
  - /kanban create in the gateway auto-subscribes the originating
    chat (platform + chat_id + thread_id). Users see
    '(subscribed — you'll be notified when t_abcd completes or
    blocks)' appended to the response.

Dashboard plugin
  - GET /stats returns board_stats (by_status, by_assignee,
    oldest_ready_age_seconds).
  - GET /tasks/:id/log returns the worker log with optional ?tail=N
    cap. 404 on unknown task, exists=false when the task has never
    spawned.
  - POST /tasks accepts idempotency_key; both Pydantic body and the
    create_task kwarg now round-trip.
  - /board attaches task.age (created/started/time_to_complete in
    seconds) so the UI can colour stale cards without recomputing.
  - Card CSS: amber border after N minutes, red border when clearly
    stuck (tier per status: running 10m/60m, ready 1h/24h, todo
    7d/30d, blocked 1h/24h).
  - Drawer: new Worker log section, auto-loads on mount, last 100 KB
    cap with on-disk path surfaced when truncated.

Kernel
  - Schema additions: tasks.idempotency_key, tasks.spawn_failures,
    tasks.worker_pid, tasks.last_spawn_error; new
    kanban_notify_subs table. All gated by _migrate_add_optional_columns
    so legacy DBs upgrade cleanly.
  - release_stale_claims / complete_task / block_task now all clear
    worker_pid so crash detection doesn't false-positive on reclaimed
    tasks.
  - read_worker_log fixed: tail-skip no longer eats one-giant-line
    logs (common with child processes that don't flush newlines
    before dying).

Tests (tests/hermes_cli/test_kanban_core_functionality.py, 28 new)
  - Idempotency: same key returns existing, archived doesn't block,
    no key never collides
  - Circuit breaker: auto-blocks after limit, success resets counter,
    workspace-resolution failure counts against budget
  - Aliveness: _pid_alive helper, detect_crashed_workers reclaims
    exited child
  - Daemon: runs and stops cleanly via stop_event, survives a tick
    exception
  - Stats + task_age helpers
  - Notify subs: CRUD, cursor advances, distinct-thread is a separate row
  - GC: events-only-for-terminal-tasks, old worker logs deleted
  - Log: rotation keeps one generation, read_worker_log tail
  - CLI: bulk complete/archive/unblock/block, create with
    --idempotency-key, stats --json, notify-subscribe+list, log
    missing task, gc reports counts
  - run_slash parity: smoke-tests every registered verb (23
    invocations); none may raise or return empty string

Full kanban test suite: 234/234 pass under scripts/run_tests.sh
(60 original + 30 dashboard plugin + 28 new core + 116 command
registry). Live smoke covers /stats, idempotency, age, log endpoint
with and without content, log?tail= truncation signal, 404 on unknown
task.

Docs (website/docs/user-guide/features/kanban.md)
  - 'Core concepts' rewritten: new statuses (triage), idempotency key,
    dispatcher-as-daemon-not-cron with circuit breaker behaviour
    documented.
  - Quick start swapped to daemon. New systemd section covers user
    service install.
  - New sections: idempotent create, bulk verbs, gateway
    notifications, out-of-scope single-host note (kanban.db is local;
    don't expect multi-host).
  - CLI reference updated for every new verb, every new flag.
2026-04-26 13:01:09 -07:00

23 KiB
Raw Blame History

sidebar_position, title, description
sidebar_position title description
12 Kanban (Multi-Agent Board) Durable SQLite-backed task board for coordinating multiple Hermes profiles

Kanban — Multi-Agent Profile Collaboration

Hermes Kanban is a durable task board, shared across all your Hermes profiles, that lets multiple named agents collaborate on work without fragile in-process subagent swarms. Every task is a row in ~/.hermes/kanban.db; every handoff is a row anyone can read and write; every worker is a full OS process with its own identity.

This is the shape that covers the workloads delegate_task can't:

  • Research triage — parallel researchers + analyst + writer, human-in-the-loop.
  • Scheduled ops — recurring daily briefs that build a journal over weeks.
  • Digital twins — persistent named assistants (inbox-triage, ops-review) that accumulate memory over time.
  • Engineering pipelines — decompose → implement in parallel worktrees → review → iterate → PR.
  • Fleet work — one specialist managing N subjects (50 social accounts, 12 monitored services).

For the full design rationale, comparative analysis against Cline Kanban / Paperclip / NanoClaw / Google Gemini Enterprise, and the eight canonical collaboration patterns, see docs/hermes-kanban-v1-spec.pdf in the repository.

Kanban vs. delegate_task

They look similar; they are not the same primitive.

delegate_task Kanban
Shape RPC call (fork → join) Durable message queue + state machine
Parent Blocks until child returns Fire-and-forget after create
Child identity Anonymous subagent Named profile with persistent memory
Resumability None — failed = failed Block → unblock → re-run; crash → reclaim
Human in the loop Not supported Comment / unblock at any point
Agents per task One call = one subagent N agents over task's life (retry, review, follow-up)
Audit trail Lost on context compression Durable rows in SQLite forever
Coordination Hierarchical (caller → callee) Peer — any profile reads/writes any task

One-sentence distinction: delegate_task is a function call; Kanban is a work queue where every handoff is a row any profile (or human) can see and edit.

Use delegate_task when the parent agent needs a short reasoning answer before continuing, no humans involved, result goes back into the parent's context.

Use Kanban when work crosses agent boundaries, needs to survive restarts, might need human input, might be picked up by a different role, or needs to be discoverable after the fact.

They coexist: a kanban worker may call delegate_task internally during its run.

Core concepts

  • Task — a row with title, optional body, one assignee (a profile name), status (triage | todo | ready | running | blocked | done | archived), optional tenant namespace, optional idempotency key (dedup for retried automation).
  • Linktask_links row recording a parent → child dependency. The dispatcher promotes todo → ready when all parents are done.
  • Comment — the inter-agent protocol. Agents and humans append comments; when a worker is (re-)spawned it reads the full comment thread as part of its context.
  • Workspace — the directory a worker operates in. Three kinds:
    • scratch (default) — fresh tmp dir under ~/.hermes/kanban/workspaces/<id>/.
    • dir:<path> — an existing shared directory (Obsidian vault, mail ops dir, per-account folder).
    • worktree — a git worktree under .worktrees/<id>/ for coding tasks.
  • Dispatcher — a long-lived loop that, every N seconds (default 60): reclaims stale claims, reclaims crashed workers (PID gone but TTL not yet expired), promotes ready tasks, atomically claims, spawns assigned profiles. Runs as hermes kanban daemon (foreground) or as a systemd user service. After ~5 consecutive spawn failures on the same task the dispatcher auto-blocks it with the last error as the reason — prevents thrashing on tasks whose profile doesn't exist, workspace can't mount, etc.
  • Tenant — optional string namespace. One specialist fleet can serve multiple businesses (--tenant business-a) with data isolation by workspace path and memory key prefix.

Quick start

# 1. Create the board
hermes kanban init

# 2. Start the dispatcher (foreground; Ctrl-C to stop)
hermes kanban daemon &

# 3. Create a task
hermes kanban create "research AI funding landscape" --assignee researcher

# 4. Watch activity live
hermes kanban watch

# 5. See the board
hermes kanban list
hermes kanban stats

Running the dispatcher as a service

For production, install the systemd user unit shipped at plugins/kanban/systemd/hermes-kanban-dispatcher.service:

mkdir -p ~/.config/systemd/user
cp plugins/kanban/systemd/hermes-kanban-dispatcher.service \
   ~/.config/systemd/user/
systemctl --user daemon-reload
systemctl --user enable --now hermes-kanban-dispatcher.service
systemctl --user status hermes-kanban-dispatcher
journalctl --user -u hermes-kanban-dispatcher -f   # follow logs

Without a running dispatcher ready tasks stay where they are — hermes kanban init will remind you of this on first run.

Idempotent create (for automation / webhooks)

# First call creates the task. Any subsequent call with the same key
# returns the existing task id instead of duplicating.
hermes kanban create "nightly ops review" \
    --assignee ops \
    --idempotency-key "nightly-ops-$(date -u +%Y-%m-%d)" \
    --json

Bulk CLI verbs

All the lifecycle verbs accept multiple ids so you can clean up a batch in one command:

hermes kanban complete t_abc t_def t_hij --result "batch wrap"
hermes kanban archive  t_abc t_def t_hij
hermes kanban unblock  t_abc t_def
hermes kanban block    t_abc "need input" --ids t_def t_hij

The worker skill

Any profile that should be able to work kanban tasks must load the kanban-worker skill. It teaches the worker the full lifecycle:

  1. On spawn, read $HERMES_KANBAN_TASK env var.
  2. Run hermes kanban context $HERMES_KANBAN_TASK to read title + body + parent results + full comment thread.
  3. cd $HERMES_KANBAN_WORKSPACE and do the work there.
  4. Complete with hermes kanban complete <id> --result "<summary>", or block with hermes kanban block <id> "<reason>" if stuck.

Load it with:

hermes skills install devops/kanban-worker

The orchestrator skill

A well-behaved orchestrator does not do the work itself. It decomposes the user's goal into tasks, links them, assigns each to a specialist, and steps back. The kanban-orchestrator skill encodes this: anti-temptation rules, a standard specialist roster (researcher, writer, analyst, backend-eng, reviewer, ops), and a decomposition playbook.

Load it into your orchestrator profile:

hermes skills install devops/kanban-orchestrator

For best results, pair it with a profile whose toolsets are restricted to board operations (kanban, gateway, memory) so the orchestrator literally cannot execute implementation tasks even if it tries.

Dashboard (GUI)

The /kanban CLI and slash command are enough to run the board headlessly, but a visual board is often the right interface for humans-in-the-loop: triage, cross-profile supervision, reading comment threads, and dragging cards between columns. Hermes ships this as a bundled dashboard plugin at plugins/kanban/ — not a core feature, not a separate service — following the model laid out in Extending the Dashboard.

Open it with:

hermes kanban init      # one-time: create kanban.db if not already present
hermes dashboard        # "Kanban" tab appears in the nav, after "Skills"

What the plugin gives you

  • A Kanban tab showing one column per status: triage, todo, ready, running, blocked, done (plus archived when the toggle is on).
    • triage is the parking column for rough ideas a specifier is expected to flesh out. Tasks created with hermes kanban create --triage (or via the Triage column's inline create) land here and the dispatcher leaves them alone until a human or specifier promotes them to todo / ready.
  • Cards show the task id, title, priority badge, tenant tag, assigned profile, comment/link counts, a progress pill (N/M children done when the task has dependents), and "created N ago". A per-card checkbox enables multi-select.
  • Per-profile lanes inside Running — toolbar checkbox toggles sub-grouping of the Running column by assignee.
  • Live updates via WebSocket — the plugin tails the append-only task_events table on a short poll interval; the board reflects changes the instant any profile (CLI, gateway, or another dashboard tab) acts. Reloads are debounced so a burst of events triggers a single refetch.
  • Drag-drop cards between columns to change status. The drop sends PATCH /api/plugins/kanban/tasks/:id which routes through the same kanban_db code the CLI uses — the three surfaces can never drift. Moves into destructive statuses (done, archived, blocked) prompt for confirmation. Touch devices use a pointer-based fallback so the board is usable from a tablet.
  • Inline create — click + on any column header to type a title, assignee, priority, and (optionally) a parent task from a dropdown over every existing task. Creating from the Triage column automatically parks the new task in triage.
  • Multi-select with bulk actions — shift/ctrl-click a card or tick its checkbox to add it to the selection. A bulk action bar appears at the top with batch status transitions, archive, and reassign (by profile dropdown, or "(unassign)"). Destructive batches confirm first. Per-id partial failures are reported without aborting the rest.
  • Click a card (without shift/ctrl) to open a side drawer (Escape or click-outside closes) with:
    • Editable title — click the heading to rename.
    • Editable assignee / priority — click the meta row to rewrite.
    • Editable description — markdown-rendered by default (headings, bold, italic, inline code, fenced code, http(s) / mailto: links, bullet lists), with an "edit" button that swaps in a textarea. Markdown rendering is a tiny, XSS-safe renderer — every substitution runs on HTML-escaped input, only http(s) / mailto: links pass through, and target="_blank" + rel="noopener noreferrer" are always set.
    • Dependency editor — chip list of parents and children, each with an × to unlink, plus dropdowns over every other task to add a new parent or child. Cycle attempts are rejected server-side with a clear message.
    • Status action row (→ triage / → ready / → running / block / unblock / complete / archive) with confirm prompts for destructive transitions.
    • Result section (also markdown-rendered), comment thread with Enter-to-submit, the last 20 events.
  • Toolbar filters — free-text search, tenant dropdown (defaults to dashboard.kanban.default_tenant from config.yaml), assignee dropdown, "show archived" toggle, "lanes by profile" toggle, and a Nudge dispatcher button so you don't have to wait for the next 60 s tick.

Visually the target is the familiar Linear / Fusion layout: dark theme, column headers with counts, coloured status dots, pill chips for priority and tenant. The plugin reads only theme CSS vars (--color-*, --radius, --font-mono, ...), so it reskins automatically with whichever dashboard theme is active.

Architecture

The GUI is strictly a read-through-the-DB + write-through-kanban_db layer with no domain logic of its own:

┌────────────────────────┐      WebSocket (tails task_events)
│   React SPA (plugin)   │ ◀──────────────────────────────────┐
│   HTML5 drag-and-drop  │                                    │
└──────────┬─────────────┘                                    │
           │ REST over fetchJSON                              │
           ▼                                                  │
┌────────────────────────┐     writes call kanban_db.*        │
│  FastAPI router        │     directly — same code path      │
│  plugins/kanban/       │     the CLI /kanban verbs use      │
│  dashboard/plugin_api.py                                    │
└──────────┬─────────────┘                                    │
           │                                                  │
           ▼                                                  │
┌────────────────────────┐                                    │
│  ~/.hermes/kanban.db   │ ───── append task_events ──────────┘
│  (WAL, shared)         │
└────────────────────────┘

REST surface

All routes are mounted under /api/plugins/kanban/ and protected by the dashboard's ephemeral session token:

Method Path Purpose
GET /board?tenant=<name>&include_archived=… Full board grouped by status column, plus tenants + assignees for filter dropdowns
GET /tasks/:id Task + comments + events + links
POST /tasks Create (wraps kanban_db.create_task, accepts triage: bool and parents: [id, …])
PATCH /tasks/:id Status / assignee / priority / title / body / result
POST /tasks/bulk Apply the same patch (status / archive / assignee / priority) to every id in ids. Per-id failures reported without aborting siblings
POST /tasks/:id/comments Append a comment
POST /links Add a dependency (parent_idchild_id)
DELETE /links?parent_id=…&child_id=… Remove a dependency
POST /dispatch?max=…&dry_run=… Nudge the dispatcher — skip the 60 s wait
GET /config Read dashboard.kanban preferences from config.yamldefault_tenant, lane_by_profile, include_archived_by_default, render_markdown
WS /events?since=<event_id> Live stream of task_events rows

Every handler is a thin wrapper — the plugin is ~700 lines of Python (router + WebSocket tail + bulk batcher + config reader) and adds no new business logic. A tiny _conn() helper auto-initializes kanban.db on every read and write, so a fresh install works whether the user opened the dashboard first, hit the REST API directly, or ran hermes kanban init.

Dashboard config

Any of these keys under dashboard.kanban in ~/.hermes/config.yaml changes the tab's defaults — the plugin reads them at load time via GET /config:

dashboard:
  kanban:
    default_tenant: acme              # preselects the tenant filter
    lane_by_profile: true             # default for the "lanes by profile" toggle
    include_archived_by_default: false
    render_markdown: true             # set false for plain <pre> rendering

Each key is optional and falls back to the shown default.

Security model

The dashboard's HTTP auth middleware explicitly skips /api/plugins/ — plugin routes are unauthenticated by design because the dashboard binds to localhost by default. That means the kanban REST surface is reachable from any process on the host.

The WebSocket takes one additional step: it requires the dashboard's ephemeral session token as a ?token=… query parameter (browsers can't set Authorization on an upgrade request), matching the pattern used by the in-browser PTY bridge.

If you run hermes dashboard --host 0.0.0.0, every plugin route — kanban included — becomes reachable from the network. Don't do that on a shared host. The board contains task bodies, comments, and workspace paths; an attacker reaching these routes gets read access to your entire collaboration surface and can also create / reassign / archive tasks.

Tasks in ~/.hermes/kanban.db are profile-agnostic on purpose (that's the coordination primitive). If you open the dashboard with hermes -p <profile> dashboard, the board still shows tasks created by any other profile on the host. Same user owns all profiles, but this is worth knowing if multiple personas coexist.

Live updates

task_events is an append-only SQLite table with a monotonic id. The WebSocket endpoint holds each client's last-seen event id and pushes new rows as they land. When a burst of events arrives, the frontend reloads the (very cheap) board endpoint — simpler and more correct than trying to patch local state from every event kind. WAL mode means the read loop never blocks the dispatcher's BEGIN IMMEDIATE claim transactions.

Extending it

The plugin uses the standard Hermes dashboard plugin contract — see Extending the Dashboard for the full manifest reference, shell slots, page-scoped slots, and the Plugin SDK. Extra columns, custom card chrome, tenant-filtered layouts, or full tab.override replacements are all expressible without forking this plugin.

To disable without removing: add dashboard.plugins.kanban.enabled: false to config.yaml (or delete plugins/kanban/dashboard/manifest.json).

Scope boundary

The GUI is deliberately thin. Everything the plugin does is reachable from the CLI; the plugin just makes it comfortable for humans. Auto-assignment, budgets, governance gates, and org-chart views remain user-space — a router profile, another plugin, or a reuse of tools/approval.py — exactly as listed in the out-of-scope section of the design spec.

CLI command reference

hermes kanban init                                     # create kanban.db + print daemon hint
hermes kanban create "<title>" [--body ...] [--assignee <profile>]
                                [--parent <id>]... [--tenant <name>]
                                [--workspace scratch|worktree|dir:<path>]
                                [--priority N] [--triage] [--idempotency-key KEY]
                                [--json]
hermes kanban list [--mine] [--assignee P] [--status S] [--tenant T] [--archived] [--json]
hermes kanban show <id> [--json]
hermes kanban assign <id> <profile>                    # or 'none' to unassign
hermes kanban link <parent_id> <child_id>
hermes kanban unlink <parent_id> <child_id>
hermes kanban claim <id> [--ttl SECONDS]
hermes kanban comment <id> "<text>" [--author NAME]

# Bulk verbs — accept multiple ids:
hermes kanban complete <id>... [--result "..."]
hermes kanban block <id> "<reason>" [--ids <id>...]
hermes kanban unblock <id>...
hermes kanban archive <id>...

hermes kanban tail <id>                                # follow a single task's event stream
hermes kanban watch [--assignee P] [--tenant T]        # live stream ALL events to the terminal
        [--kinds completed,blocked,…] [--interval SECS]
hermes kanban dispatch [--dry-run] [--max N]           # one-shot pass
        [--failure-limit N] [--json]
hermes kanban daemon [--interval SECS] [--max N]       # long-lived loop
        [--failure-limit N] [--pidfile PATH] [-v]
hermes kanban stats [--json]                           # per-status + per-assignee counts
hermes kanban log <id> [--tail BYTES]                  # worker log from ~/.hermes/kanban/logs/
hermes kanban notify-subscribe <id>                    # gateway bridge hook (used by /kanban in the gateway)
        --platform <name> --chat-id <id> [--thread-id <id>] [--user-id <id>]
hermes kanban notify-list [<id>] [--json]
hermes kanban notify-unsubscribe <id>
        --platform <name> --chat-id <id> [--thread-id <id>]
hermes kanban context <id>                             # what a worker sees
hermes kanban gc [--event-retention-days N]            # workspaces + old events + old logs
        [--log-retention-days N]

All commands are also available as a slash command in the gateway (/kanban list, /kanban comment t_abc "need docs", etc.). The slash command bypasses the running-agent guard, so you can /kanban unblock a stuck worker while the main agent is still chatting.

Collaboration patterns

The board supports these eight patterns without any new primitives:

Pattern Shape Example
P1 Fan-out N siblings, same role "research 5 angles in parallel"
P2 Pipeline role chain: scout → editor → writer daily brief assembly
P3 Voting / quorum N siblings + 1 aggregator 3 researchers → 1 reviewer picks
P4 Long-running journal same profile + shared dir + cron Obsidian vault
P5 Human-in-the-loop worker blocks → user comments → unblock ambiguous decisions
P6 @mention inline routing from prose @reviewer look at this
P7 Thread-scoped workspace /kanban here in a thread per-project gateway threads
P8 Fleet farming one profile, N subjects 50 social accounts
P9 Triage specifier rough idea → triage → specifier expands body → todo "turn this one-liner into a spec' task"

For worked examples of each, see docs/hermes-kanban-v1-spec.pdf.

Multi-tenant usage

When one specialist fleet serves multiple businesses, tag each task with a tenant:

hermes kanban create "monthly report" \
    --assignee researcher \
    --tenant business-a \
    --workspace dir:~/tenants/business-a/data/

Workers receive $HERMES_TENANT and namespace their memory writes by prefix. The board, the dispatcher, and the profile definitions are all shared; only the data is scoped.

Gateway notifications

When you run /kanban create … from the gateway (Telegram, Discord, Slack, etc.), the originating chat is automatically subscribed to the new task. The gateway's background notifier polls task_events every few seconds and delivers one message per terminal event (completed, blocked, spawn_auto_blocked, crashed) to that chat. Completed tasks also send the first line of the worker's --result so you see the outcome without having to /kanban show.

You can manage subscriptions explicitly from the CLI — useful when a script / cron job wants to notify a chat it didn't originate from:

hermes kanban notify-subscribe t_abcd \
    --platform telegram --chat-id 12345678 --thread-id 7
hermes kanban notify-list
hermes kanban notify-unsubscribe t_abcd \
    --platform telegram --chat-id 12345678 --thread-id 7

A subscription removes itself automatically once the task reaches done or archived; no cleanup needed.

Out of scope

Kanban is deliberately single-host. ~/.hermes/kanban.db is a local SQLite file and the dispatcher spawns workers on the same machine. Running a shared board across two hosts is not supported — there's no coordination primitive for "worker X on host A, worker Y on host B," and the crash-detection path assumes PIDs are host-local. If you need multi-host, run an independent board per host and use delegate_task / a message queue to bridge them.

Design spec

The complete design — architecture, concurrency correctness, comparison with other systems, implementation plan, risks, open questions — lives in docs/hermes-kanban-v1-spec.pdf. Read that before filing any behavior-change PR.