mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-06 10:47:12 +08:00
Merge branch 'main' of github.com:NousResearch/hermes-agent into bb/gui
This commit is contained in:
@@ -88,7 +88,7 @@ Hermes supports seven terminal backends. Each determines where the agent's shell
|
||||
```yaml
|
||||
terminal:
|
||||
backend: local # local | docker | ssh | modal | daytona | vercel_sandbox | singularity
|
||||
cwd: "." # Working directory ("." = current dir for local, "/root" for containers)
|
||||
cwd: "." # Gateway/cron working directory (CLI always uses launch dir)
|
||||
timeout: 180 # Per-command timeout in seconds
|
||||
env_passthrough: [] # Env var names to forward to sandboxed execution (terminal + execute_code)
|
||||
singularity_image: "docker://nikolaik/python-nodejs:python3.11-nodejs20" # Container image for Singularity backend
|
||||
|
||||
@@ -45,28 +45,33 @@ Opening any port on an internet facing machine is a security risk. You should no
|
||||
|
||||
## Running the dashboard
|
||||
|
||||
The built-in web dashboard can run alongside the gateway as a separate container.
|
||||
|
||||
To run the dashboard as its own container, point it at the gateway's health endpoint so it can detect gateway status across containers:
|
||||
The built-in web dashboard runs as an optional side-process inside the same container as the gateway. Set `HERMES_DASHBOARD=1` and expose port `9119` alongside the gateway's `8642`:
|
||||
|
||||
```sh
|
||||
docker run -d \
|
||||
--name hermes-dashboard \
|
||||
--name hermes \
|
||||
--restart unless-stopped \
|
||||
-v ~/.hermes:/opt/data \
|
||||
-p 8642:8642 \
|
||||
-p 9119:9119 \
|
||||
-e GATEWAY_HEALTH_URL=http://$HOST_IP:8642 \
|
||||
nousresearch/hermes-agent dashboard
|
||||
-e HERMES_DASHBOARD=1 \
|
||||
nousresearch/hermes-agent gateway run
|
||||
```
|
||||
|
||||
Replace `$HOST_IP` with the IP address of the machine running the gateway container (e.g. `192.168.1.100`), or use a Docker network hostname if both containers share a network (see the [Compose example](#docker-compose-example) below).
|
||||
The entrypoint starts `hermes dashboard` in the background (running as the non-root `hermes` user) before `exec`-ing the main command. Dashboard output is prefixed with `[dashboard]` in `docker logs` so it's easy to separate from gateway logs.
|
||||
|
||||
| Environment variable | Description | Default |
|
||||
|---------------------|-------------|---------|
|
||||
| `GATEWAY_HEALTH_URL` | Base URL of the gateway's API server, e.g. `http://gateway:8642` | *(unset — local PID check only)* |
|
||||
| `GATEWAY_HEALTH_TIMEOUT` | Health probe timeout in seconds | `3` |
|
||||
| `HERMES_DASHBOARD` | Set to `1` (or `true` / `yes`) to launch the dashboard alongside the main command | *(unset — dashboard not started)* |
|
||||
| `HERMES_DASHBOARD_HOST` | Bind address for the dashboard HTTP server | `0.0.0.0` |
|
||||
| `HERMES_DASHBOARD_PORT` | Port for the dashboard HTTP server | `9119` |
|
||||
| `HERMES_DASHBOARD_TUI` | Set to `1` to expose the in-browser Chat tab (embedded `hermes --tui` via PTY/WebSocket) | *(unset)* |
|
||||
|
||||
Without `GATEWAY_HEALTH_URL`, the dashboard falls back to local process detection — which only works when the gateway runs in the same container or on the same host.
|
||||
The default `HERMES_DASHBOARD_HOST=0.0.0.0` is required for the host to reach the dashboard through the published port; the entrypoint automatically passes `--insecure` to `hermes dashboard` in that case. Override to `127.0.0.1` if you want to restrict the dashboard to in-container access only (e.g. behind a reverse proxy in a sidecar).
|
||||
|
||||
:::note
|
||||
The dashboard side-process is **not supervised** — if it crashes, it stays down until the container restarts. Running it as a separate container is not supported: the dashboard's gateway-liveness detection requires a shared PID namespace with the gateway process.
|
||||
:::
|
||||
|
||||
## Running interactively (CLI chat)
|
||||
|
||||
@@ -102,7 +107,7 @@ The `/opt/data` volume is the single source of truth for all Hermes state. It ma
|
||||
| `skins/` | Custom CLI skins |
|
||||
|
||||
:::warning
|
||||
Never run two Hermes **gateway** containers against the same data directory simultaneously — session files and memory stores are not designed for concurrent write access. Running a dashboard container alongside the gateway is safe since the dashboard only reads data.
|
||||
Never run two Hermes **gateway** containers against the same data directory simultaneously — session files and memory stores are not designed for concurrent write access.
|
||||
:::
|
||||
|
||||
## Multi-profile support
|
||||
@@ -188,49 +193,24 @@ services:
|
||||
restart: unless-stopped
|
||||
command: gateway run
|
||||
ports:
|
||||
- "8642:8642"
|
||||
- "8642:8642" # gateway API
|
||||
- "9119:9119" # dashboard (only reached when HERMES_DASHBOARD=1)
|
||||
volumes:
|
||||
- ~/.hermes:/opt/data
|
||||
networks:
|
||||
- hermes-net
|
||||
# Uncomment to forward specific env vars instead of using .env file:
|
||||
# environment:
|
||||
# - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
|
||||
# - OPENAI_API_KEY=${OPENAI_API_KEY}
|
||||
# - TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN}
|
||||
environment:
|
||||
- HERMES_DASHBOARD=1
|
||||
# Uncomment to forward specific env vars instead of using .env file:
|
||||
# - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
|
||||
# - OPENAI_API_KEY=${OPENAI_API_KEY}
|
||||
# - TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN}
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 4G
|
||||
cpus: "2.0"
|
||||
|
||||
dashboard:
|
||||
image: nousresearch/hermes-agent:latest
|
||||
container_name: hermes-dashboard
|
||||
restart: unless-stopped
|
||||
command: dashboard --host 0.0.0.0 --insecure
|
||||
ports:
|
||||
- "9119:9119"
|
||||
volumes:
|
||||
- ~/.hermes:/opt/data
|
||||
environment:
|
||||
- GATEWAY_HEALTH_URL=http://hermes:8642
|
||||
networks:
|
||||
- hermes-net
|
||||
depends_on:
|
||||
- hermes
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 512M
|
||||
cpus: "0.5"
|
||||
|
||||
networks:
|
||||
hermes-net:
|
||||
driver: bridge
|
||||
```
|
||||
|
||||
Start with `docker compose up -d` and view logs with `docker compose logs -f`.
|
||||
Start with `docker compose up -d` and view logs with `docker compose logs -f`. Dashboard output is prefixed with `[dashboard]` so it's easy to filter from gateway logs.
|
||||
|
||||
## Resource limits
|
||||
|
||||
@@ -273,6 +253,7 @@ The entrypoint script (`docker/entrypoint.sh`) bootstraps the data volume on fir
|
||||
- Copies default `config.yaml` if missing
|
||||
- Copies default `SOUL.md` if missing
|
||||
- Syncs bundled skills using a manifest-based approach (preserves user edits)
|
||||
- Optionally launches `hermes dashboard` as a background side-process when `HERMES_DASHBOARD=1` (see [Running the dashboard](#running-the-dashboard))
|
||||
- Then runs `hermes` with whatever arguments you pass
|
||||
|
||||
## Upgrading
|
||||
|
||||
@@ -10,7 +10,11 @@ hermes dashboard # opens http://127.0.0.1:9119 in your browser
|
||||
# click Kanban in the left nav
|
||||
```
|
||||
|
||||
The dashboard is the most comfortable place to learn the system. Everything you see here is also available via `hermes kanban <verb>` on the CLI — the two surfaces share the same SQLite database at `~/.hermes/kanban.db`.
|
||||
The dashboard is the most comfortable place for **you** to watch the system. Agent workers the dispatcher spawns never see the dashboard or the CLI — they drive the board through a dedicated `kanban_*` [toolset](./kanban#how-workers-interact-with-the-board) (`kanban_show`, `kanban_complete`, `kanban_block`, `kanban_heartbeat`, `kanban_comment`, `kanban_create`, `kanban_link`). All three surfaces — dashboard, CLI, worker tools — route through the same per-board SQLite DB (`~/.hermes/kanban.db` for the default board, `~/.hermes/kanban/boards/<slug>/kanban.db` for any board you create later), so each board is consistent no matter which side of the fence a change came from.
|
||||
|
||||
This tutorial uses the `default` board throughout. If you want multiple isolated queues (one per project / repo / domain), see [Boards (multi-project)](./kanban#boards-multi-project) in the overview — the same CLI / dashboard / worker flows apply per board, and workers physically cannot see tasks on other boards.
|
||||
|
||||
Throughout the tutorial, **code blocks labelled `bash` are commands *you* run.** Code blocks labelled `# worker tool calls` are what the spawned worker's model emits as tool calls — shown here so you can see the loop end-to-end, not because you'd ever run them yourself.
|
||||
|
||||
## The board at a glance
|
||||
|
||||
@@ -57,22 +61,32 @@ hermes kanban create "Write auth integration tests" \
|
||||
|
||||
Because `API` has `SCHEMA` as its parent, and `tests` has `API` as its parent, only `SCHEMA` starts in `ready`. The other two sit in `todo` until their parents complete. This is the dependency promotion engine doing its job — no other worker will pick up the test-writing until there's an API to test.
|
||||
|
||||
Claim the schema task, do the work, hand off:
|
||||
On the next dispatcher tick (60s by default, or immediately if you hit **Nudge dispatcher**) the `backend-dev` profile spawns as a worker with `HERMES_KANBAN_TASK=$SCHEMA` in its env. Here's what the worker's tool-call loop looks like from inside the agent:
|
||||
|
||||
```bash
|
||||
hermes kanban claim $SCHEMA
|
||||
```python
|
||||
# worker tool calls — NOT commands you run
|
||||
kanban_show()
|
||||
# → returns title, body, worker_context, parents, prior attempts, comments
|
||||
|
||||
# (you design the schema, commit, etc.)
|
||||
# (worker reads worker_context, uses terminal/file tools to design the schema,
|
||||
# write migrations, run its own checks, commit — the real work happens here)
|
||||
|
||||
hermes kanban complete $SCHEMA \
|
||||
--summary "users(id, email, pw_hash), sessions(id, user_id, jti, expires_at); refresh tokens stored as sessions with type='refresh'" \
|
||||
--metadata '{
|
||||
kanban_heartbeat(note="schema drafted, writing migrations now")
|
||||
|
||||
kanban_complete(
|
||||
summary="users(id, email, pw_hash), sessions(id, user_id, jti, expires_at); "
|
||||
"refresh tokens stored as sessions with type='refresh'",
|
||||
metadata={
|
||||
"changed_files": ["migrations/001_users.sql", "migrations/002_sessions.sql"],
|
||||
"decisions": ["bcrypt for hashing", "JWT for session tokens", "7-day refresh, 15-min access"]
|
||||
}'
|
||||
"decisions": ["bcrypt for hashing", "JWT for session tokens",
|
||||
"7-day refresh, 15-min access"],
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
When `SCHEMA` hits `done`, the dependency engine promotes `API` to `ready` automatically. The API worker, when it picks up, will read `SCHEMA`'s summary and metadata in its context — so it knows the schema decisions without re-reading a long design doc.
|
||||
`kanban_show` defaults `task_id` to `$HERMES_KANBAN_TASK`, so the worker doesn't need to know its own id. `kanban_complete` writes the summary + metadata onto the current `task_runs` row, closes that run, and transitions the task to `done` — all in one atomic hop through `kanban_db`.
|
||||
|
||||
When `SCHEMA` hits `done`, the dependency engine promotes `API` to `ready` automatically. The API worker, when it picks up, will call `kanban_show()` and see `SCHEMA`'s summary and metadata attached to the parent handoff — so it knows the schema decisions without re-reading a long design doc.
|
||||
|
||||
Click the completed schema task on the board and the drawer shows everything:
|
||||
|
||||
@@ -80,7 +94,7 @@ Click the completed schema task on the board and the drawer shows everything:
|
||||
|
||||
The Run History section at the bottom is the key addition. One attempt: outcome `completed`, worker `@backend-dev`, duration, timestamp, and the handoff summary in full. The metadata blob (`changed_files`, `decisions`) is stored on the run too and surfaced to any downstream worker that reads this parent.
|
||||
|
||||
On the CLI:
|
||||
You can inspect the same data from your terminal at any time — these commands are **you** peeking at the board, not the worker:
|
||||
|
||||
```bash
|
||||
hermes kanban show $SCHEMA
|
||||
@@ -125,7 +139,7 @@ Now filter the board to `content-ops` (or just search for "Transcribe") and you
|
||||
|
||||
Two transcribes done, one running, two ready waiting for the next dispatcher tick. The In Progress column is grouped by profile (the "Lanes by profile" default) so you see each worker's active task without scanning a mixed list. The dispatcher will promote the next ready task to running as soon as the current one completes. With three daemons working on three assignee pools in parallel, the whole content queue drains without further human input.
|
||||
|
||||
**Everything Story 1 said about structured handoff still applies here.** A translator worker completing a call can pass `--summary "translated 4 pages, style matched existing marketing voice"` and `--metadata '{"duration_seconds": 720, "tokens_used": 2100}'` — useful for analytics and for any downstream task that depends on this one.
|
||||
**Everything Story 1 said about structured handoff still applies here.** A translator worker completing a call emits `kanban_complete(summary="translated 4 pages, style matched existing marketing voice", metadata={"duration_seconds": 720, "tokens_used": 2100})` — useful for analytics and for any downstream task that depends on this one.
|
||||
|
||||
## Story 3 — Role pipeline with retry
|
||||
|
||||
@@ -137,32 +151,64 @@ The dashboard view, filtered by `auth-project`:
|
||||
|
||||
Three-stage chain visible at once: `Spec: password reset flow` (DONE, pm), `Implement password reset flow` (DONE, backend-dev), `Review password reset PR` (READY, reviewer). Each has its parent in green at the bottom and children as dependencies.
|
||||
|
||||
The interesting one is the implementation task, because it was blocked and retried:
|
||||
The interesting one is the implementation task, because it was blocked and retried. Here's the full three-agent choreography, shown as the tool calls each worker's model makes:
|
||||
|
||||
```bash
|
||||
# PM completes the spec with acceptance criteria in metadata
|
||||
hermes kanban complete $SPEC \
|
||||
--summary "spec approved; POST /forgot-password sends email, GET /reset/:token renders form, POST /reset applies new password" \
|
||||
--metadata '{"acceptance": [
|
||||
```python
|
||||
# --- PM worker spawns on $SPEC and writes the acceptance criteria ---
|
||||
# worker tool calls
|
||||
kanban_show()
|
||||
kanban_complete(
|
||||
summary="spec approved; POST /forgot-password sends email, "
|
||||
"GET /reset/:token renders form, POST /reset applies new password",
|
||||
metadata={"acceptance": [
|
||||
"expired token returns 410",
|
||||
"reused last-3 password returns 400 with message",
|
||||
"successful reset invalidates all active sessions"
|
||||
]}'
|
||||
"successful reset invalidates all active sessions",
|
||||
]},
|
||||
)
|
||||
# → $SPEC is done; $IMPL auto-promotes from todo to ready
|
||||
|
||||
# Engineer claims + implements, but review blocks it for missing strength check
|
||||
hermes kanban claim $IMPL
|
||||
hermes kanban block $IMPL "Review: password strength check missing, reset link isn't single-use (can be replayed within 30min)"
|
||||
# --- Engineer worker spawns on $IMPL (first attempt) ---
|
||||
# worker tool calls
|
||||
kanban_show() # reads $SPEC's summary + acceptance metadata in worker_context
|
||||
# (engineer writes code, runs tests, opens PR)
|
||||
# Reviewer feedback arrives — engineer decides the concerns are valid and blocks
|
||||
kanban_block(
|
||||
reason="Review: password strength check missing, reset link isn't "
|
||||
"single-use (can be replayed within 30min)",
|
||||
)
|
||||
# → $IMPL transitions to blocked; run 1 closes with outcome='blocked'
|
||||
```
|
||||
|
||||
# Engineer iterates, resolves, completes
|
||||
Now you (the human, or a separate reviewer profile) read the block reason, decide the fix direction is clear, and unblock from the dashboard's "Unblock" button — or from the CLI / slash command:
|
||||
|
||||
```bash
|
||||
hermes kanban unblock $IMPL
|
||||
hermes kanban claim $IMPL
|
||||
hermes kanban complete $IMPL \
|
||||
--summary "added zxcvbn strength check, reset tokens are now single-use (stored + deleted on success)" \
|
||||
--metadata '{
|
||||
"changed_files": ["auth/reset.py", "auth/tests/test_reset.py", "migrations/003_single_use_reset_tokens.sql"],
|
||||
# or from a chat: /kanban unblock $IMPL
|
||||
```
|
||||
|
||||
The dispatcher promotes `$IMPL` back to `ready` and, on the next tick, respawns the `backend-dev` worker. This second spawn is a **new run** on the same task:
|
||||
|
||||
```python
|
||||
# --- Engineer worker spawns on $IMPL (second attempt) ---
|
||||
# worker tool calls
|
||||
kanban_show()
|
||||
# → worker_context now includes the run 1 block reason, so this worker knows
|
||||
# which two things to fix instead of re-reading the whole spec
|
||||
# (engineer adds zxcvbn check, makes reset tokens single-use, re-runs tests)
|
||||
kanban_complete(
|
||||
summary="added zxcvbn strength check, reset tokens are now single-use "
|
||||
"(stored + deleted on success)",
|
||||
metadata={
|
||||
"changed_files": [
|
||||
"auth/reset.py",
|
||||
"auth/tests/test_reset.py",
|
||||
"migrations/003_single_use_reset_tokens.sql",
|
||||
],
|
||||
"tests_run": 11,
|
||||
"review_iteration": 2
|
||||
}'
|
||||
"review_iteration": 2,
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
Click the implementation task. The drawer shows **two attempts**:
|
||||
@@ -178,7 +224,7 @@ The reviewer picks up next. When they open `Review password reset PR`, they see:
|
||||
|
||||

|
||||
|
||||
The parent link is the completed implementation. When the reviewer's worker calls `build_worker_context`, it pulls the parent's most-recent-completed-run summary + metadata — so the reviewer reads "added zxcvbn strength check, reset tokens are now single-use" and has the list of changed files in hand before looking at a diff.
|
||||
The parent link is the completed implementation. When the reviewer's worker spawns on `Review password reset PR` and calls `kanban_show()`, the returned `worker_context` includes the parent's most-recent-completed-run summary + metadata — so the reviewer reads "added zxcvbn strength check, reset tokens are now single-use" and has the list of changed files in hand before looking at a diff.
|
||||
|
||||
## Story 4 — Circuit breaker and crash recovery
|
||||
|
||||
@@ -234,18 +280,18 @@ The drawer shows the full two-attempt history:
|
||||
|
||||
Run 1 — `crashed`, with the error `OOM kill at row 2.3M (process 99999 gone)`. Run 2 — `completed`, with `"strategy": "chunked with LIMIT + WHERE id > last_id"` in its metadata. The retrying worker saw the crash of run 1 in its context and picked a safer strategy; the metadata makes it obvious to a future observer (or postmortem writer) what changed.
|
||||
|
||||
## Structured handoff — why `--summary` and `--metadata` matter
|
||||
## Structured handoff — why `summary` and `metadata` matter
|
||||
|
||||
In every story above, workers passed `--summary` and `--metadata` on completion. That's not decoration — it's the primary handoff channel between stages of a workflow.
|
||||
In every story above, workers called `kanban_complete(summary=..., metadata=...)` at the end. That's not decoration — it's the primary handoff channel between stages of a workflow.
|
||||
|
||||
When a worker on task B reads its context, it gets:
|
||||
When a worker on task B is spawned and calls `kanban_show()`, the `worker_context` it gets back includes:
|
||||
|
||||
- B's **prior attempts** (previous runs: outcome, summary, error, metadata) so a retrying worker doesn't repeat a failed path.
|
||||
- **Parent task results** — for each parent, the most-recent completed run's summary and metadata — so downstream workers see why and how the upstream work was done.
|
||||
|
||||
This replaces the "dig through comments and the work output" dance that plagues flat kanban systems. A PM writes acceptance criteria in the spec's metadata, and the engineer's worker sees them structurally. An engineer records which tests they ran and how many passed, and the reviewer's worker has that list in hand before opening a diff.
|
||||
This replaces the "dig through comments and the work output" dance that plagues flat kanban systems. A PM writes acceptance criteria in the spec's metadata, and the engineer's worker sees them structurally in the parent handoff. An engineer records which tests they ran and how many passed, and the reviewer's worker has that list in hand before opening a diff.
|
||||
|
||||
The bulk-close guard exists because this data is per-run. `hermes kanban complete a b c --summary X` is refused — copy-pasting the same summary to three tasks is almost always wrong. Bulk close without the handoff flags still works for the common "I finished a pile of admin tasks" case.
|
||||
The bulk-close guard exists because this data is per-run. `hermes kanban complete a b c --summary X` (you, from the CLI) is refused — copy-pasting the same summary to three tasks is almost always wrong. Bulk close without the handoff flags still works for the common "I finished a pile of admin tasks" case. The tool surface doesn't expose a bulk variant at all; `kanban_complete` is always single-task-at-a-time for the same reason.
|
||||
|
||||
## Inspecting a task currently running
|
||||
|
||||
|
||||
@@ -10,6 +10,15 @@ description: "Durable SQLite-backed task board for coordinating multiple Hermes
|
||||
|
||||
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.
|
||||
|
||||
### Two surfaces: the model talks through tools, you talk through the CLI
|
||||
|
||||
The board has two front doors, both backed by the same `~/.hermes/kanban.db`:
|
||||
|
||||
- **Agents drive the board through a dedicated `kanban_*` toolset** — `kanban_show`, `kanban_complete`, `kanban_block`, `kanban_heartbeat`, `kanban_comment`, `kanban_create`, `kanban_link`. The dispatcher spawns each worker with these tools already in its schema; the model reads its task and hands work off by calling them directly, *not* by shelling out to `hermes kanban`. See [How workers interact with the board](#how-workers-interact-with-the-board) below.
|
||||
- **You (and scripts, and cron) drive the board through `hermes kanban …`** on the CLI, `/kanban …` as a slash command, or the dashboard. These are for humans and automation — the places without a tool-calling model behind them.
|
||||
|
||||
Both surfaces route through the same `kanban_db` layer, so reads see a consistent view and writes can't drift. The rest of this page shows CLI examples because they're easy to copy-paste, but every CLI verb has a tool-call equivalent the model uses.
|
||||
|
||||
This is the shape that covers the workloads `delegate_task` can't:
|
||||
|
||||
- **Research triage** — parallel researchers + analyst + writer, human-in-the-loop.
|
||||
@@ -45,36 +54,131 @@ They coexist: a kanban worker may call `delegate_task` internally during its run
|
||||
|
||||
## Core concepts
|
||||
|
||||
- **Board** — a standalone queue of tasks with its own SQLite DB, workspaces
|
||||
directory, and dispatcher loop. A single install can have many boards
|
||||
(e.g. one per project, repo, or domain); see [Boards (multi-project)](#boards-multi-project)
|
||||
below. Single-project users stay on the `default` board and never see the
|
||||
word "board" outside this docs section.
|
||||
- **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).
|
||||
- **Link** — `task_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>/`.
|
||||
- `scratch` (default) — fresh tmp dir under `~/.hermes/kanban/workspaces/<id>/` (or `~/.hermes/kanban/boards/<slug>/workspaces/<id>/` on non-default boards).
|
||||
- `dir:<path>` — an existing shared directory (Obsidian vault, mail ops dir, per-account folder). **Must be an absolute path.** Relative paths like `dir:../tenants/foo/` are rejected at dispatch because they'd resolve against whatever CWD the dispatcher happens to be in, which is ambiguous and a confused-deputy escape vector. The path is otherwise trusted — it's your box, your filesystem, the worker runs with your uid. This is the trusted-local-user threat model; kanban is single-host by design.
|
||||
- `worktree` — a git worktree under `.worktrees/<id>/` for coding tasks. Worker-side `git worktree add` creates it.
|
||||
- **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 **inside the gateway** by default (`kanban.dispatch_in_gateway: true`). 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.
|
||||
- **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 **inside the gateway** by default (`kanban.dispatch_in_gateway: true`). One dispatcher sweeps all boards per tick; workers are spawned with `HERMES_KANBAN_BOARD` pinned so they can't see other boards. 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 *within* a board. One specialist fleet can serve multiple businesses (`--tenant business-a`) with data isolation by workspace path and memory key prefix. Tenants are a soft filter; boards are the hard isolation boundary.
|
||||
|
||||
## Boards (multi-project)
|
||||
|
||||
Boards let you separate unrelated streams of work — one per project, repo,
|
||||
or domain — into isolated queues. A new install has exactly one board
|
||||
called `default` (DB at `~/.hermes/kanban.db` for back-compat). Users who
|
||||
only want one stream of work never need to know about boards; the feature
|
||||
is opt-in.
|
||||
|
||||
Per-board isolation is absolute:
|
||||
|
||||
- Separate SQLite DB per board (`~/.hermes/kanban/boards/<slug>/kanban.db`).
|
||||
- Separate `workspaces/` and `logs/` directories.
|
||||
- Workers spawned for a task see **only** their board's tasks — the
|
||||
dispatcher sets `HERMES_KANBAN_BOARD` in the child env and every
|
||||
`kanban_*` tool the worker has access to reads it.
|
||||
- Linking tasks across boards is not allowed (keeps the schema simple; if
|
||||
you really need cross-project refs, use free-text mentions and look
|
||||
them up by id manually).
|
||||
|
||||
### Managing boards from the CLI
|
||||
|
||||
```bash
|
||||
# See what's on disk. Fresh installs show only "default".
|
||||
hermes kanban boards list
|
||||
|
||||
# Create a new board.
|
||||
hermes kanban boards create atm10-server \
|
||||
--name "ATM10 Server" \
|
||||
--description "Minecraft modded server ops" \
|
||||
--icon 🎮 \
|
||||
--switch # optional: make it the active board
|
||||
|
||||
# Operate on a specific board without switching.
|
||||
hermes kanban --board atm10-server list
|
||||
hermes kanban --board atm10-server create "Restart ATM server" --assignee ops
|
||||
|
||||
# Change which board is "current" for subsequent calls.
|
||||
hermes kanban boards switch atm10-server
|
||||
hermes kanban boards show # who's active right now?
|
||||
|
||||
# Rename the display name (the slug is immutable — it's the directory name).
|
||||
hermes kanban boards rename atm10-server "ATM10 (Prod)"
|
||||
|
||||
# Archive (default) — moves the board's dir to boards/_archived/<slug>-<ts>/.
|
||||
# Recoverable by moving the dir back.
|
||||
hermes kanban boards rm atm10-server
|
||||
|
||||
# Hard delete — `rm -rf` the board dir. No recovery.
|
||||
hermes kanban boards rm atm10-server --delete
|
||||
```
|
||||
|
||||
Board resolution order (highest precedence first):
|
||||
|
||||
1. Explicit `--board <slug>` on the CLI call.
|
||||
2. `HERMES_KANBAN_BOARD` env var (set by the dispatcher when spawning a
|
||||
worker, so workers can't see other boards).
|
||||
3. `~/.hermes/kanban/current` — the slug persisted by `hermes kanban
|
||||
boards switch`.
|
||||
4. `default`.
|
||||
|
||||
Slugs are validated: lowercase alphanumerics + hyphens + underscores, 1-64
|
||||
chars, must start with alphanumeric. Uppercase input is auto-downcased.
|
||||
Anything else (slashes, spaces, dots, `..`) is rejected at the CLI layer
|
||||
so path-traversal tricks can't name a board.
|
||||
|
||||
### Managing boards from the dashboard
|
||||
|
||||
`hermes dashboard` → Kanban tab shows a board switcher at the top as soon
|
||||
as more than one board exists (or any board has tasks). Single-board users
|
||||
see only a small `+ New board` button; the switcher is hidden until it
|
||||
matters.
|
||||
|
||||
- **Board dropdown** — pick the active board. Your selection is saved to
|
||||
the browser's `localStorage` so it persists across reloads without
|
||||
shifting the CLI's `current` pointer out from under a terminal you left
|
||||
open.
|
||||
- **+ New board** — opens a modal asking for slug, display name,
|
||||
description, and icon. Option to auto-switch to the new board.
|
||||
- **Archive** — only shown on non-`default` boards. Confirms, then moves
|
||||
the board dir to `boards/_archived/`.
|
||||
|
||||
All dashboard API endpoints accept `?board=<slug>` for board scoping. The
|
||||
events WebSocket is pinned to a board at connection time; switching in
|
||||
the UI opens a fresh WS against the new board.
|
||||
|
||||
|
||||
## Quick start
|
||||
|
||||
The commands below are **you** (the human) setting up the board and creating tasks. Once a task is assigned, the dispatcher spawns the assigned profile as a worker, and from there **the model drives the task through `kanban_*` tool calls, not CLI commands** — see [How workers interact with the board](#how-workers-interact-with-the-board).
|
||||
|
||||
```bash
|
||||
# 1. Create the board
|
||||
# 1. Create the board (you)
|
||||
hermes kanban init
|
||||
|
||||
# 2. Start the gateway (hosts the embedded dispatcher)
|
||||
hermes gateway start
|
||||
|
||||
# 3. Create a task
|
||||
# 3. Create a task (you — or an orchestrator agent via kanban_create)
|
||||
hermes kanban create "research AI funding landscape" --assignee researcher
|
||||
|
||||
# 4. Watch activity live
|
||||
# 4. Watch activity live (you)
|
||||
hermes kanban watch
|
||||
|
||||
# 5. See the board
|
||||
# 5. See the board (you)
|
||||
hermes kanban list
|
||||
hermes kanban stats
|
||||
```
|
||||
|
||||
When the dispatcher picks up `t_abcd` and spawns the `researcher` profile, the very first thing that worker's model does is call `kanban_show()` to read its task. It doesn't run `hermes kanban show t_abcd`.
|
||||
|
||||
### Gateway-embedded dispatcher (default)
|
||||
|
||||
The dispatcher runs inside the gateway process. Nothing to install, no
|
||||
@@ -127,22 +231,61 @@ hermes kanban block t_abc "need input" --ids t_def t_hij
|
||||
|
||||
## How workers interact with the board
|
||||
|
||||
When the dispatcher spawns a worker, it sets `HERMES_KANBAN_TASK` in the child's env. That env var is the gate for a dedicated **kanban toolset** — 7 tools that the normal agent schema never sees:
|
||||
**Workers do not shell out to `hermes kanban`.** When the dispatcher spawns a worker it sets `HERMES_KANBAN_TASK=t_abcd` in the child's env, and that env var flips on a dedicated **kanban toolset** in the model's schema — seven tools that read and mutate the board directly via the Python `kanban_db` layer, same as the CLI does. A running worker calls these like any other tool; it never sees or needs the `hermes kanban` CLI.
|
||||
|
||||
| Tool | Purpose |
|
||||
|---|---|
|
||||
| `kanban_show` | Read the current task (title, body, prior attempts, parent handoffs, comments, full `worker_context`). Defaults to the env's task id. |
|
||||
| `kanban_complete` | Finish with `summary` + `metadata` structured handoff. |
|
||||
| `kanban_block` | Escalate for human input. |
|
||||
| `kanban_heartbeat` | Signal liveness during long operations. |
|
||||
| `kanban_comment` | Append to the task thread. |
|
||||
| `kanban_create` | (Orchestrators) fan out into child tasks. |
|
||||
| `kanban_link` | (Orchestrators) add dependency edges after the fact. |
|
||||
| Tool | Purpose | Required params |
|
||||
|---|---|---|
|
||||
| `kanban_show` | Read the current task (title, body, prior attempts, parent handoffs, comments, full pre-formatted `worker_context`). Defaults to the env's task id. | — |
|
||||
| `kanban_complete` | Finish with `summary` + `metadata` structured handoff. | at least one of `summary` / `result` |
|
||||
| `kanban_block` | Escalate for human input with a `reason`. | `reason` |
|
||||
| `kanban_heartbeat` | Signal liveness during long operations. Pure side-effect. | — |
|
||||
| `kanban_comment` | Append a durable note to the task thread. | `task_id`, `body` |
|
||||
| `kanban_create` | (Orchestrators) fan out into child tasks with an `assignee`, optional `parents`, `skills`, etc. | `title`, `assignee` |
|
||||
| `kanban_link` | (Orchestrators) add a `parent_id → child_id` dependency edge after the fact. | `parent_id`, `child_id` |
|
||||
|
||||
**Why tools and not just shelling to `hermes kanban`?** Three reasons:
|
||||
A typical worker turn looks like:
|
||||
|
||||
1. **Backend portability.** Workers whose terminal tool points at a remote backend (Docker / Modal / Singularity / SSH) would run `hermes kanban complete` inside the container where `hermes` isn't installed and the DB isn't mounted. The kanban tools run in the agent's own Python process and always reach `~/.hermes/kanban.db` regardless of terminal backend.
|
||||
2. **No shell-quoting fragility.** Passing `--metadata '{"files": [...]}'` through shlex + argparse is a latent footgun. Structured tool args skip it.
|
||||
```
|
||||
# Model's tool calls, in order:
|
||||
kanban_show() # no args — uses HERMES_KANBAN_TASK
|
||||
# (model reads the returned worker_context, does the work via terminal/file tools)
|
||||
kanban_heartbeat(note="halfway through — 4 of 8 files transformed")
|
||||
# (more work)
|
||||
kanban_complete(
|
||||
summary="migrated limiter.py to token-bucket; added 14 tests, all pass",
|
||||
metadata={"changed_files": ["limiter.py", "tests/test_limiter.py"], "tests_run": 14},
|
||||
)
|
||||
```
|
||||
|
||||
An **orchestrator** worker fans out instead:
|
||||
|
||||
```
|
||||
kanban_show()
|
||||
kanban_create(
|
||||
title="research ICP funding 2024-2026",
|
||||
assignee="researcher-a",
|
||||
body="focus on seed + series A, North America, AI-adjacent",
|
||||
)
|
||||
# → returns {"task_id": "t_r1", ...}
|
||||
kanban_create(title="research ICP funding — EU angle", assignee="researcher-b", body="…")
|
||||
# → returns {"task_id": "t_r2", ...}
|
||||
kanban_create(
|
||||
title="synthesize findings into launch brief",
|
||||
assignee="writer",
|
||||
parents=["t_r1", "t_r2"], # promotes to ready when both complete
|
||||
body="one-pager, 300 words, neutral tone",
|
||||
)
|
||||
kanban_complete(summary="decomposed into 2 research tasks + 1 writer; linked dependencies")
|
||||
```
|
||||
|
||||
The three "(Orchestrators)" tools — `kanban_create`, `kanban_link`, and `kanban_comment` on foreign tasks — are available to every worker; the convention (enforced by the `kanban-orchestrator` skill) is that worker profiles don't fan out and orchestrator profiles don't execute.
|
||||
|
||||
### Why tools instead of shelling to `hermes kanban`
|
||||
|
||||
Three reasons:
|
||||
|
||||
1. **Backend portability.** Workers whose terminal tool points at a remote backend (Docker / Modal / Singularity / SSH) would run `hermes kanban complete` *inside* the container, where `hermes` isn't installed and `~/.hermes/kanban.db` isn't mounted. The kanban tools run in the agent's own Python process and always reach `~/.hermes/kanban.db` regardless of terminal backend.
|
||||
2. **No shell-quoting fragility.** Passing `--metadata '{"files": [...]}'` through shlex + argparse is a latent footgun. Structured tool args skip it entirely.
|
||||
3. **Better errors.** Tool results are structured JSON the model can reason about, not stderr strings it has to parse.
|
||||
|
||||
**Zero schema footprint on normal sessions.** A regular `hermes chat` session has zero `kanban_*` tools in its schema. The `check_fn` on each tool only returns True when `HERMES_KANBAN_TASK` is set, which only happens when the dispatcher spawned this process. No tool bloat for users who never touch kanban.
|
||||
@@ -151,14 +294,14 @@ The `kanban-worker` and `kanban-orchestrator` skills teach the model which tool
|
||||
|
||||
### 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:
|
||||
Any profile that should be able to work kanban tasks must load the `kanban-worker` skill. It teaches the worker the full lifecycle in **tool calls**, not CLI commands:
|
||||
|
||||
1. On spawn, call `kanban_show()` to read title + body + parent handoffs + prior attempts + full comment thread.
|
||||
2. `cd $HERMES_KANBAN_WORKSPACE` and do the work there.
|
||||
2. `cd $HERMES_KANBAN_WORKSPACE` (via the terminal tool) and do the work there.
|
||||
3. Call `kanban_heartbeat(note="...")` every few minutes during long operations.
|
||||
4. Complete with `kanban_complete(summary="...", metadata={...})`, or `kanban_block(reason="...")` if stuck.
|
||||
|
||||
Load it with:
|
||||
Load it with (this one is **you**, installing into a profile — not a tool call):
|
||||
|
||||
```bash
|
||||
hermes skills install devops/kanban-worker
|
||||
@@ -168,22 +311,9 @@ The dispatcher also auto-passes `--skills kanban-worker` when spawning every wor
|
||||
|
||||
### Pinning extra skills to a specific task
|
||||
|
||||
Sometimes a single task needs specialist context the assignee profile doesn't carry by default — a translation job that needs the `translation` skill, a review task that needs `github-code-review`, a security audit that needs `security-pr-audit`. Rather than editing the assignee's profile every time, attach the skills directly to the task:
|
||||
Sometimes a single task needs specialist context the assignee profile doesn't carry by default — a translation job that needs the `translation` skill, a review task that needs `github-code-review`, a security audit that needs `security-pr-audit`. Rather than editing the assignee's profile every time, attach the skills directly to the task.
|
||||
|
||||
```bash
|
||||
# CLI — repeat --skill for each extra skill
|
||||
hermes kanban create "translate README to Japanese" \
|
||||
--assignee linguist \
|
||||
--skill translation
|
||||
|
||||
# Multiple skills
|
||||
hermes kanban create "audit auth flow" \
|
||||
--assignee reviewer \
|
||||
--skill security-pr-audit \
|
||||
--skill github-code-review
|
||||
```
|
||||
|
||||
From the dashboard's inline create form, type the skills comma-separated into the **skills** field. From another agent (orchestrator pattern), use `kanban_create(skills=[...])`:
|
||||
**From an orchestrator agent** (the usual case — one agent routing work to another), use the `kanban_create` tool's `skills` array:
|
||||
|
||||
```
|
||||
kanban_create(
|
||||
@@ -191,13 +321,53 @@ kanban_create(
|
||||
assignee="linguist",
|
||||
skills=["translation"],
|
||||
)
|
||||
|
||||
kanban_create(
|
||||
title="audit auth flow",
|
||||
assignee="reviewer",
|
||||
skills=["security-pr-audit", "github-code-review"],
|
||||
)
|
||||
```
|
||||
|
||||
**From a human (CLI / slash command)**, repeat `--skill` for each one:
|
||||
|
||||
```bash
|
||||
hermes kanban create "translate README to Japanese" \
|
||||
--assignee linguist \
|
||||
--skill translation
|
||||
|
||||
hermes kanban create "audit auth flow" \
|
||||
--assignee reviewer \
|
||||
--skill security-pr-audit \
|
||||
--skill github-code-review
|
||||
```
|
||||
|
||||
**From the dashboard**, type the skills comma-separated into the **skills** field of the inline create form.
|
||||
|
||||
These skills are **additive** to the built-in `kanban-worker` — the dispatcher emits one `--skills <name>` flag for each (and for the built-in), so the worker spawns with all of them loaded. The skill names must match skills that are actually installed on the assignee's profile (run `hermes skills list` to see what's available); there's no runtime install.
|
||||
|
||||
### 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.
|
||||
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 as tool-call patterns: anti-temptation rules, a standard specialist roster (`researcher`, `writer`, `analyst`, `backend-eng`, `reviewer`, `ops`), and a decomposition playbook keyed on `kanban_create` / `kanban_link` / `kanban_comment`.
|
||||
|
||||
A canonical orchestrator turn (two parallel researchers handing off to a writer):
|
||||
|
||||
```
|
||||
# Goal from user: "draft a launch post on the ICP funding landscape"
|
||||
kanban_create(title="research ICP funding, NA angle", assignee="researcher-a", body="…") # → t_r1
|
||||
kanban_create(title="research ICP funding, EU angle", assignee="researcher-b", body="…") # → t_r2
|
||||
kanban_create(
|
||||
title="synthesize ICP funding research into launch post draft",
|
||||
assignee="writer",
|
||||
parents=["t_r1", "t_r2"], # promoted to 'ready' when both researchers complete
|
||||
body="one-pager, neutral tone, cite sources inline",
|
||||
) # → t_w1
|
||||
# Optional: add cross-cutting deps discovered later without re-creating tasks
|
||||
kanban_link(parent_id="t_r1", child_id="t_followup")
|
||||
kanban_complete(
|
||||
summary="decomposed into 2 parallel research tasks → 1 synthesis task; writer starts when both researchers finish",
|
||||
)
|
||||
```
|
||||
|
||||
Load it into your orchestrator profile:
|
||||
|
||||
@@ -324,6 +494,8 @@ The GUI is deliberately thin. Everything the plugin does is reachable from the C
|
||||
|
||||
## CLI command reference
|
||||
|
||||
This is the surface **you** (or scripts, cron, the dashboard) use to drive the board. Workers running inside the dispatcher use the `kanban_*` [tool surface](#how-workers-interact-with-the-board) for the same operations — the CLI here and the tools there both route through `kanban_db`, so the two surfaces agree by construction.
|
||||
|
||||
```
|
||||
hermes kanban init # create kanban.db + print daemon hint
|
||||
hermes kanban create "<title>" [--body ...] [--assignee <profile>]
|
||||
@@ -369,7 +541,57 @@ hermes kanban gc [--event-retention-days N] # workspaces + old events
|
||||
[--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.
|
||||
All commands are also available as a slash command in the interactive CLI and in the messaging gateway (see [`/kanban` slash command](#kanban-slash-command) below).
|
||||
|
||||
## `/kanban` slash command {#kanban-slash-command}
|
||||
|
||||
Every `hermes kanban <action>` verb is also reachable as `/kanban <action>` — from inside an interactive `hermes chat` session **and** from any gateway platform (Telegram, Discord, Slack, WhatsApp, Signal, Matrix, Mattermost, email, SMS). Both surfaces call the exact same `hermes_cli.kanban.run_slash()` entry point that reuses the `hermes kanban` argparse tree, so the argument surface, flags, and output format are identical across CLI, `/kanban`, and `hermes kanban`. You don't have to leave the chat to drive the board.
|
||||
|
||||
```
|
||||
/kanban list
|
||||
/kanban show t_abcd
|
||||
/kanban create "write launch post" --assignee writer --parent t_research
|
||||
/kanban comment t_abcd "looks good, ship it"
|
||||
/kanban unblock t_abcd
|
||||
/kanban dispatch --max 3
|
||||
```
|
||||
|
||||
Quote multi-word arguments the same way you would on a shell — `run_slash` parses the rest of the line with `shlex.split`, so `"..."` and `'...'` both work.
|
||||
|
||||
### Mid-run usage: `/kanban` bypasses the running-agent guard
|
||||
|
||||
The gateway normally queues slash commands and user messages while an agent is still thinking — that's what stops you from accidentally starting a second turn while the first is in flight. **`/kanban` is explicitly exempted from this guard.** The board lives in `~/.hermes/kanban.db`, not in the running agent's state, so reads (`list`, `show`, `context`, `tail`, `watch`, `stats`, `runs`) and writes (`comment`, `unblock`, `block`, `assign`, `archive`, `create`, `link`, …) all go through immediately, even mid-turn.
|
||||
|
||||
This is the whole point of the separation:
|
||||
|
||||
- A worker blocks waiting on a peer → you send `/kanban unblock t_abcd` from your phone and the dispatcher picks the peer up on its next tick. The blocked worker isn't interrupted — it just stops being blocked.
|
||||
- You spot a card that needs human context → `/kanban comment t_xyz "use the 2026 schema, not 2025"` lands on the task thread and the *next* run of that task will read it in `kanban_show()`.
|
||||
- You want to know what your fleet is doing without stopping the orchestrator → `/kanban list --mine` or `/kanban stats` inspects the board without touching your main conversation.
|
||||
|
||||
### Auto-subscribe on `/kanban create` (gateway only)
|
||||
|
||||
When you create a task from the gateway with `/kanban create "…"`, the originating chat (platform + chat id + thread id) is automatically subscribed to that task's terminal events (`completed`, `blocked`, `gave_up`, `crashed`, `timed_out`). You'll get one message back per terminal event — including the first line of the worker's result summary on `completed` — without having to poll or remember the task id.
|
||||
|
||||
```
|
||||
you> /kanban create "transcribe today's podcast" --assignee transcriber
|
||||
bot> Created t_9fc1a3 (ready, assignee=transcriber)
|
||||
(subscribed — you'll be notified when t_9fc1a3 completes or blocks)
|
||||
|
||||
… ~8 minutes later …
|
||||
|
||||
bot> ✓ t_9fc1a3 completed by transcriber
|
||||
transcribed 42 minutes, saved to podcast/2026-05-04.md
|
||||
```
|
||||
|
||||
Subscriptions auto-remove themselves once the task reaches `done` or `archived`. If you script a create with `--json` (machine output) the auto-subscribe is skipped — the assumption is that scripted callers want to manage subscriptions explicitly via `/kanban notify-subscribe`.
|
||||
|
||||
### Output truncation in messaging
|
||||
|
||||
Gateway platforms have practical message-length caps. If `/kanban list`, `/kanban show`, or `/kanban tail` produce more than ~3800 characters of output, the response is truncated with a `… (truncated; use \`hermes kanban …\` in your terminal for full output)` footer. The CLI surface has no such cap.
|
||||
|
||||
### Autocomplete
|
||||
|
||||
In the interactive CLI, typing `/kanban ` and hitting Tab cycles through the built-in subcommand list (`list`, `ls`, `show`, `create`, `assign`, `link`, `unlink`, `claim`, `comment`, `complete`, `block`, `unblock`, `archive`, `tail`, `dispatch`, `context`, `init`, `gc`). The remaining verbs listed in the CLI reference above (`watch`, `stats`, `runs`, `log`, `assignees`, `heartbeat`, `notify-subscribe`, `notify-list`, `notify-unsubscribe`, `daemon`) also work — they're just not in the autocomplete hint list yet.
|
||||
|
||||
## Collaboration patterns
|
||||
|
||||
@@ -424,16 +646,26 @@ A task is a logical unit of work; a **run** is one attempt to execute it. When t
|
||||
|
||||
Why two tables instead of just mutating the task: you need **full attempt history** for real-world postmortems ("the second reviewer attempt got to approve, the third merged"), and you need a clean place to hang per-attempt metadata — which files changed, which tests ran, which findings a reviewer noted. Those are run facts, not task facts.
|
||||
|
||||
Runs are also where **structured handoff** lives. When a worker completes a task it can pass:
|
||||
Runs are also where **structured handoff** lives. When a worker completes a task (via `kanban_complete(...)`) it can pass:
|
||||
|
||||
- `--result "<short log line>"` — goes on the task row as before (for back-compat).
|
||||
- `--summary "<human handoff>"` — goes on the run; downstream children see it in their `build_worker_context`.
|
||||
- `--metadata '{"changed_files": [...], "tests_run": 12}'` — JSON dict on the run; children see it serialized alongside the summary.
|
||||
- `summary` (tool param) / `--summary` (CLI) — human handoff; goes on the run; downstream children see it in their `build_worker_context`.
|
||||
- `metadata` (tool param) / `--metadata` (CLI) — free-form JSON dict on the run; children see it serialized alongside the summary.
|
||||
- `result` (tool param) / `--result` (CLI) — short log line that goes on the task row (legacy field, kept for back-compat).
|
||||
|
||||
Downstream children read the most recent completed run's summary + metadata for each parent. Retrying workers read the prior attempts on their own task (outcome, summary, error) so they don't repeat a path that already failed.
|
||||
|
||||
```
|
||||
# What a worker actually does — a tool call, from inside the agent loop:
|
||||
kanban_complete(
|
||||
summary="implemented token bucket, keys on user_id with IP fallback, all tests pass",
|
||||
metadata={"changed_files": ["limiter.py", "tests/test_limiter.py"], "tests_run": 14},
|
||||
result="rate limiter shipped",
|
||||
)
|
||||
```
|
||||
|
||||
The same handoff is reachable from the CLI when you (the human) need to close out a task a worker can't — e.g. a task that was abandoned, or one you marked done manually from the dashboard:
|
||||
|
||||
```bash
|
||||
# Worker completes with a structured handoff:
|
||||
hermes kanban complete t_abcd \
|
||||
--result "rate limiter shipped" \
|
||||
--summary "implemented token bucket, keys on user_id with IP fallback, all tests pass" \
|
||||
|
||||
@@ -9,6 +9,11 @@ description: "Extend Hermes with custom tools, hooks, and integrations via the p
|
||||
|
||||
Hermes has a plugin system for adding custom tools, hooks, and integrations without modifying core code.
|
||||
|
||||
If you want to create a custom tool for yourself, your team, or one project,
|
||||
this is usually the right path. The developer guide's
|
||||
[Adding Tools](/docs/developer-guide/adding-tools) page is for built-in Hermes
|
||||
core tools that live in `tools/` and `toolsets.py`.
|
||||
|
||||
**→ [Build a Hermes Plugin](/docs/guides/build-a-hermes-plugin)** — step-by-step guide with a complete working example.
|
||||
|
||||
## Quick overview
|
||||
@@ -42,6 +47,8 @@ description: A minimal example plugin
|
||||
```python
|
||||
"""Minimal Hermes plugin — registers a tool and a hook."""
|
||||
|
||||
import json
|
||||
|
||||
|
||||
def register(ctx):
|
||||
# --- Tool: hello_world ---
|
||||
@@ -60,11 +67,18 @@ def register(ctx):
|
||||
},
|
||||
}
|
||||
|
||||
def handle_hello(params):
|
||||
def handle_hello(params, **kwargs):
|
||||
del kwargs
|
||||
name = params.get("name", "World")
|
||||
return f"Hello, {name}! 👋 (from the hello-world plugin)"
|
||||
return json.dumps({"success": True, "greeting": f"Hello, {name}!"})
|
||||
|
||||
ctx.register_tool("hello_world", schema, handle_hello)
|
||||
ctx.register_tool(
|
||||
name="hello_world",
|
||||
toolset="hello_world",
|
||||
schema=schema,
|
||||
handler=handle_hello,
|
||||
description="Return a friendly greeting for the given name.",
|
||||
)
|
||||
|
||||
# --- Hook: log every tool call ---
|
||||
def on_tool_call(tool_name, params, result):
|
||||
@@ -81,7 +95,7 @@ Project-local plugins under `./.hermes/plugins/` are disabled by default. Enable
|
||||
|
||||
| Capability | How |
|
||||
|-----------|-----|
|
||||
| Add tools | `ctx.register_tool(name, schema, handler)` |
|
||||
| Add tools | `ctx.register_tool(name=..., toolset=..., schema=..., handler=...)` |
|
||||
| Add hooks | `ctx.register_hook("post_tool_call", callback)` |
|
||||
| Add slash commands | `ctx.register_command(name, handler, description)` — adds `/name` in CLI and gateway sessions |
|
||||
| Add CLI commands | `ctx.register_cli_command(name, help, setup_fn, handler_fn)` — adds `hermes <plugin> <subcommand>` |
|
||||
|
||||
@@ -26,11 +26,15 @@ Open WebUI talks to Hermes server-to-server, so you do not need `API_SERVER_CORS
|
||||
|
||||
### 1. Enable the API server
|
||||
|
||||
Add to `~/.hermes/.env`:
|
||||
```bash
|
||||
hermes config set API_SERVER_ENABLED true
|
||||
hermes config set API_SERVER_KEY your-secret-key
|
||||
```
|
||||
|
||||
`hermes config set` auto-routes the flag to `config.yaml` and the secret to `~/.hermes/.env`. If the gateway is already running, restart it so the change takes effect:
|
||||
|
||||
```bash
|
||||
API_SERVER_ENABLED=true
|
||||
API_SERVER_KEY=your-secret-key
|
||||
hermes gateway stop && hermes gateway
|
||||
```
|
||||
|
||||
### 2. Start Hermes Agent gateway
|
||||
@@ -45,12 +49,25 @@ You should see:
|
||||
[API Server] API server listening on http://127.0.0.1:8642
|
||||
```
|
||||
|
||||
### 3. Start Open WebUI
|
||||
### 3. Verify the API server is reachable
|
||||
|
||||
```bash
|
||||
curl -s http://127.0.0.1:8642/health
|
||||
# {"status": "ok", ...}
|
||||
|
||||
curl -s -H "Authorization: Bearer your-secret-key" http://127.0.0.1:8642/v1/models
|
||||
# {"object":"list","data":[{"id":"hermes-agent", ...}]}
|
||||
```
|
||||
|
||||
If `/health` fails, the gateway didn't pick up `API_SERVER_ENABLED=true` — restart it. If `/v1/models` returns `401`, your `Authorization` header doesn't match `API_SERVER_KEY`.
|
||||
|
||||
### 4. Start Open WebUI
|
||||
|
||||
```bash
|
||||
docker run -d -p 3000:8080 \
|
||||
-e OPENAI_API_BASE_URL=http://host.docker.internal:8642/v1 \
|
||||
-e OPENAI_API_KEY=your-secret-key \
|
||||
-e ENABLE_OLLAMA_API=false \
|
||||
--add-host=host.docker.internal:host-gateway \
|
||||
-v open-webui:/app/backend/data \
|
||||
--name open-webui \
|
||||
@@ -58,7 +75,11 @@ docker run -d -p 3000:8080 \
|
||||
ghcr.io/open-webui/open-webui:main
|
||||
```
|
||||
|
||||
### 4. Open the UI
|
||||
`ENABLE_OLLAMA_API=false` suppresses the default Ollama backend, which would otherwise show up empty and clutter the model picker. Omit it if you actually have Ollama running alongside.
|
||||
|
||||
First launch takes 15–30 seconds: Open WebUI downloads sentence-transformer embedding models (~150MB) the first time it starts. Wait for `docker logs open-webui` to settle before opening the UI.
|
||||
|
||||
### 5. Open the UI
|
||||
|
||||
Go to **http://localhost:3000**. Create your admin account (the first user becomes admin). You should see your agent in the model dropdown (named after your profile, or **hermes-agent** for the default profile). Start chatting!
|
||||
|
||||
@@ -77,6 +98,7 @@ services:
|
||||
environment:
|
||||
- OPENAI_API_BASE_URL=http://host.docker.internal:8642/v1
|
||||
- OPENAI_API_KEY=your-secret-key
|
||||
- ENABLE_OLLAMA_API=false
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
restart: always
|
||||
@@ -181,8 +203,9 @@ With streaming enabled (the default), you'll see brief inline indicators as tool
|
||||
|
||||
- **Check the URL has `/v1` suffix**: `http://host.docker.internal:8642/v1` (not just `:8642`)
|
||||
- **Verify the gateway is running**: `curl http://localhost:8642/health` should return `{"status": "ok"}`
|
||||
- **Check model listing**: `curl http://localhost:8642/v1/models` should return a list with `hermes-agent`
|
||||
- **Check model listing**: `curl -H "Authorization: Bearer your-secret-key" http://localhost:8642/v1/models` should return a list with `hermes-agent`
|
||||
- **Docker networking**: From inside Docker, `localhost` means the container, not your host. Use `host.docker.internal` or `--network=host`.
|
||||
- **Empty Ollama backend shadowing the picker**: If you omitted `ENABLE_OLLAMA_API=false`, Open WebUI shows an empty Ollama section above your Hermes models. Restart the container with `-e ENABLE_OLLAMA_API=false` or disable Ollama in **Admin Settings → Connections**.
|
||||
|
||||
### Connection test passes but no models load
|
||||
|
||||
|
||||
@@ -293,9 +293,9 @@ Hermes Agent works in Telegram group chats with a few considerations:
|
||||
- `TELEGRAM_ALLOWED_USERS` still applies — only authorized users can trigger the bot, even in groups
|
||||
- You can keep the bot from responding to ordinary group chatter with `telegram.require_mention: true`
|
||||
- With `telegram.require_mention: true`, group messages are accepted when they are:
|
||||
- slash commands
|
||||
- replies to one of the bot's messages
|
||||
- `@botusername` mentions
|
||||
- `/command@botusername` (Telegram's bot-menu command form that includes the bot name)
|
||||
- matches for one of your configured regex wake words in `telegram.mention_patterns`
|
||||
- Use `telegram.ignored_threads` to keep Hermes silent in specific Telegram forum topics, even when the group would otherwise allow free responses or mention-triggered replies
|
||||
- If `telegram.require_mention` is left unset or false, Hermes keeps the previous open-group behavior and responds to normal group messages it can see
|
||||
|
||||
Reference in New Issue
Block a user