feat(kanban): multi-project boards — one install, many kanbans (#19653)

Adds first-class board support to kanban so users can separate unrelated
streams of work (projects, repos, domains) into isolated queues. Single-
project users stay on the 'default' board and see no UI change.

Isolation model
---------------
- Each board is a directory at `~/.hermes/kanban/boards/<slug>/` with
  its own `kanban.db`, `workspaces/`, and `logs/`. The 'default' board
  keeps its legacy path (`~/.hermes/kanban.db`) for back-compat — fresh
  installs and pre-boards users get zero migration.
- Workers spawned by the dispatcher have `HERMES_KANBAN_BOARD` pinned in
  their env alongside the existing `HERMES_KANBAN_DB` /
  `HERMES_KANBAN_WORKSPACES_ROOT` pins, so workers physically cannot see
  other boards' tasks.
- The gateway's single dispatcher loop now sweeps every board per tick;
  per-tick cost is a few extra filesystem stats.
- CAS concurrency guarantees are preserved per-board (each board is its
  own SQLite DB, same WAL+IMMEDIATE machinery as before).

CLI
---
  hermes kanban boards list|create|switch|show|rename|rm
  hermes kanban --board <slug> <any-subcommand>

Board resolution order: `--board` flag → `HERMES_KANBAN_BOARD` env →
`~/.hermes/kanban/current` file → `default`. Slug validation is strict:
lowercase alphanumerics + hyphens + underscores, 1-64 chars, starts with
alphanumeric. Uppercase is auto-downcased; slashes / dots / `..` /
control chars are rejected so boards can't name their way out of the
boards/ directory.

Passive discoverability: when more than one board exists, `hermes kanban
list` prints a one-line header ("Board: foo (2 other boards …)") so
users who stumble across multi-project never have to hunt for the
feature. Invisible for single-board installs.

Dashboard
---------
- New `BoardSwitcher` component at the top of the Kanban tab: dropdown
  with all boards + task counts, `+ New board` button, `Archive`
  button (non-default only). Hidden entirely when only `default` exists
  and is empty — single-project users never see it.
- New `NewBoardDialog` modal: slug / display name / description / icon
  + "switch to this board after creating" checkbox.
- Selected board persists to `localStorage` so browser users don't
  shift the CLI's active board out from under a terminal they left open.
- New `?board=<slug>` query param on every existing endpoint plus a
  new `/boards` CRUD surface (`GET /boards`, `POST /boards`,
  `PATCH /boards/<slug>`, `DELETE /boards/<slug>`,
  `POST /boards/<slug>/switch`).
- Events WebSocket is pinned to a board at connection time; switching
  opens a fresh WS against the new board.

Also fixes a pre-existing bug in the plugin's tenant / assignee
filters: the SDK's `Select` uses `onValueChange(value)`, not
native `onChange(event)`, so those filters silently didn't work.
New `selectChangeHandler` helper wires both signatures.

Tests
-----
49 new tests in `tests/hermes_cli/test_kanban_boards.py` covering:
slug validation (valid / invalid / auto-downcase), path resolution
(default = legacy path, named = `boards/<slug>/`, env var override),
current-board resolution chain (env > file > default), board CRUD +
archive / hard-delete, per-board connection isolation (tasks don't
leak), worker spawn env injection (`HERMES_KANBAN_BOARD`,
`HERMES_KANBAN_DB`, `HERMES_KANBAN_WORKSPACES_ROOT` all point at the
right board), and end-to-end CLI surface.

Regression surface: all 264 pre-existing kanban tests continue to pass.

Live-tested via the dashboard: created 3 boards (default,
hermes-agent, atm10-server), created tasks on each via both CLI
(`--board <slug> create`) and dashboard (inline create on the Ready
column), confirmed zero cross-board leakage, confirmed `BoardSwitcher`
+ `NewBoardDialog` work end-to-end in the browser.
This commit is contained in:
Teknium 2026-05-04 04:42:38 -07:00 committed by GitHub
parent 135b4c8b35
commit 5ec6baa400
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 2191 additions and 212 deletions

View file

@ -769,3 +769,57 @@
word-break: break-word;
font-family: var(--font-mono, ui-monospace, monospace);
}
/* -------------------------------------------------------------------------
Multi-project: board switcher + create-board dialog
------------------------------------------------------------------------- */
.hermes-kanban-boardswitcher {
border: 1px solid var(--color-border, rgba(120, 120, 140, 0.25));
border-radius: 0.5rem;
padding: 0.6rem 0.85rem;
background: var(--color-card-subtle, rgba(255, 255, 255, 0.02));
}
.hermes-kanban-boardswitcher-inner {
display: flex;
align-items: flex-end;
gap: 0.75rem;
flex-wrap: wrap;
}
.hermes-kanban-boardswitcher-compact {
display: flex;
justify-content: flex-end;
padding: 0 0.25rem;
}
.hermes-kanban-dialog-backdrop {
position: fixed;
inset: 0;
background: rgba(8, 10, 16, 0.55);
backdrop-filter: blur(2px);
z-index: 60;
display: flex;
align-items: center;
justify-content: center;
}
.hermes-kanban-dialog {
background: var(--color-card, #121421);
color: var(--color-foreground);
border: 1px solid var(--color-border, rgba(120, 120, 140, 0.25));
border-radius: 0.5rem;
padding: 1.1rem 1.2rem 1rem;
width: 28rem;
max-width: calc(100vw - 2rem);
max-height: calc(100vh - 3rem);
overflow: auto;
box-shadow: 0 18px 40px rgba(0, 0, 0, 0.5);
}
.hermes-kanban-dialog-title {
font-size: 1rem;
font-weight: 600;
margin-bottom: 0.25rem;
}
.hermes-kanban-dialog-actions {
display: flex;
justify-content: flex-end;
gap: 0.5rem;
margin-top: 1rem;
}