aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.pi/prompts/pre-release-audit.md20
-rw-r--r--AGENTS.md11
-rw-r--r--CHANGELOG.md6
-rw-r--r--CONFIGURATION.md486
-rw-r--r--CONTRIBUTING.md11
-rw-r--r--INTEGRATIONS.md240
-rw-r--r--README.md20
-rw-r--r--SKILL.md2
-rw-r--r--SOCKET_API.md1112
-rw-r--r--docs/next/CHANGELOG.md6
-rw-r--r--docs/next/CONFIGURATION.md493
-rw-r--r--docs/next/INTEGRATIONS.md284
-rw-r--r--docs/next/README.md20
-rw-r--r--docs/next/SOCKET_API.md1112
-rw-r--r--justfile14
-rw-r--r--website/src/content/docs/agents.mdx2
-rw-r--r--website/src/content/docs/cli-reference.mdx2
-rw-r--r--website/src/content/docs/integrations.mdx15
18 files changed, 57 insertions, 3799 deletions
diff --git a/.pi/prompts/pre-release-audit.md b/.pi/prompts/pre-release-audit.md
index 4ff2ecd4..f3d9ce8f 100644
--- a/.pi/prompts/pre-release-audit.md
+++ b/.pi/prompts/pre-release-audit.md
@@ -61,26 +61,26 @@ Process:
- Preserve the existing changelog style and sections: `Added`, `Changed`, `Fixed`, `Removed`, and `Breaking Changes` when applicable.
7. Audit next-release public docs.
- - Treat root `README.md`, `CONFIGURATION.md`, `INTEGRATIONS.md`, and `SOCKET_API.md` as the latest released public docs.
- - Treat `docs/next/README.md`, `docs/next/CONFIGURATION.md`, `docs/next/INTEGRATIONS.md`, and `docs/next/SOCKET_API.md` as the next-release versions.
- - Compare meaningful user-facing changes in the range against `docs/next/` first.
- - Flag missing next-release docs for new or changed features, commands, config keys, protocol behavior, integrations, defaults, and compatibility notes.
- - Compare `docs/next/` against the root docs. Flag each difference as intended to ship in this release, stale, or needing user decision.
- - Also audit `website/` and example config snippets for release readiness, but keep them aligned with the latest published release unless the user explicitly asks for prerelease docs.
+ - Treat root `README.md` and `website/src/content/docs/` as the latest released public docs.
+ - Treat `docs/next/README.md` as the next-release root README.
+ - Compare meaningful user-facing changes in the range against root README and website docs.
+ - Flag missing release docs for new or changed features, commands, config keys, protocol behavior, integrations, defaults, and compatibility notes.
+ - Compare `docs/next/README.md` against root `README.md`. Flag each difference as intended to ship in this release, stale, or needing user decision.
+ - Also audit website docs and example config snippets for release readiness.
8. Verify finalization state.
- - Before `just release`, approved `docs/next/*` files must be copied to their root counterparts.
+ - Before `just release`, approved `docs/next/README.md` must be copied to root `README.md`, approved website doc changes must be present under `website/src/content/docs/`, and the deleted root doc files must stay deleted.
- Run or recommend:
```bash
just release-docs-check
```
- - This check must include `README.md`, `CONFIGURATION.md`, `INTEGRATIONS.md`, `SOCKET_API.md`, and `CHANGELOG.md`.
+ - This check must include root `README.md`, root `CHANGELOG.md`, the removed root doc files, and the required website docs.
- Do not run `just release` unless the working tree is clean and the docs check passes.
9. Apply changes only when asked.
- Do not edit files during the audit unless the user explicitly asks you to apply fixes.
- - When asked to apply audit fixes, update `docs/next/CHANGELOG.md` and any relevant `docs/next/` files first.
- - When asked to finalize release docs, copy approved `docs/next/` files into the matching root files and run `just release-docs-check`.
+ - When asked to apply audit fixes, update `docs/next/CHANGELOG.md`, `docs/next/README.md`, and any required website docs.
+ - When asked to finalize release docs, copy approved next-release README and changelog into root, update the matching website docs, then run `just release-docs-check`.
Output format:
diff --git a/AGENTS.md b/AGENTS.md
index 6c9b1e8d..f87866a0 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -51,11 +51,10 @@ Unit tests live next to the code (`#[cfg(test)] mod tests`). If you add behavior
## Conventions
- Conventional commits, lowercase, no emojis.
-- Do not edit root `CHANGELOG.md` during normal feature or fix work. Maintainers prepare `docs/next/CHANGELOG.md` during release review unless explicitly asked to update it earlier.
-- Treat the root public docs as the latest released docs. Do not document unreleased behavior in root `README.md`, `CONFIGURATION.md`, `INTEGRATIONS.md`, `SOCKET_API.md`, or `CHANGELOG.md` during normal feature or fix work.
-- Treat `docs/next/README.md`, `docs/next/CONFIGURATION.md`, `docs/next/INTEGRATIONS.md`, `docs/next/SOCKET_API.md`, and `docs/next/CHANGELOG.md` as the next-release versions of those public docs. When unreleased work needs public documentation, update the matching file under `docs/next/` instead of the root file.
-- Before release, copy the approved `docs/next/` versions into the root public docs. `just release` blocks until each root public doc and `CHANGELOG.md` are identical to their `docs/next/` counterparts.
-- Keep website copy and config examples aligned with the latest published release unless the user explicitly asks for prerelease docs.
+- Do not edit root `README.md` or `CHANGELOG.md` during normal feature or fix work unless explicitly asked. Maintainers prepare `docs/next/README.md` and `docs/next/CHANGELOG.md` during release review.
+- Treat website docs under `website/src/content/docs/` as the latest released public docs. Do not document unreleased behavior there during normal feature or fix work.
+- Treat `docs/next/README.md` and `docs/next/CHANGELOG.md` as next-release staging for the root README and changelog.
+- Before release, copy approved next-release docs into their released locations: `docs/next/README.md` to `README.md`, `docs/next/CHANGELOG.md` to `CHANGELOG.md`, and approved website doc changes to `website/src/content/docs/`. `just release-docs-check` verifies README/changelog sync, the website docs are present, and the removed root docs stay removed.
- Put local PRDs, planning notes, and exploratory specs under `.prd/`; that directory is ignored and locally controlled.
- When a normal feature or fix commit relates to a GitHub issue, add a commit body line `refs #<issue-number>` after the subject. Use this shape:
```text
@@ -70,7 +69,7 @@ Unit tests live next to the code (`#[cfg(test)] mod tests`). If you add behavior
## Releases
-Before cutting a release, run `/pre-release-audit` to compare commits since the last tag against `docs/next/CHANGELOG.md` and `docs/next/`, then copy the approved next-release docs and changelog into the root public docs. The release script promotes the root changelog's `## Unreleased` section into the versioned entry and copies the prepared changelog back to `docs/next/CHANGELOG.md` so the next cycle starts clean.
+Before cutting a release, run `/pre-release-audit` to compare commits since the last tag against `docs/next/CHANGELOG.md` and `docs/next/`, then copy approved next-release docs into `README.md`, `CHANGELOG.md`, and the matching website docs. The release script promotes the root changelog's `## Unreleased` section into the versioned entry and copies the prepared changelog back to `docs/next/CHANGELOG.md` so the next cycle starts clean.
Default release flow:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9956a7ff..f7a80598 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -143,7 +143,7 @@
## [0.5.1] - 2026-04-25
### Added
-- Toast notifications can now be delivered through the outer terminal as desktop notifications. Configure this with `ui.toast.delivery = "terminal"`; see `CONFIGURATION.md` for details.
+- Toast notifications can now be delivered through the outer terminal as desktop notifications. Configure this with `ui.toast.delivery = "terminal"`; see the [configuration docs](https://herdr.dev/docs/configuration/) for details.
- Herdr now writes separate capped support logs for app, client, and server modes, making persistent-session issue reports easier to diagnose without unbounded log growth.
- The bundled opencode plugin now reports question prompts as blocked while waiting for user input, then returns to working or idle when answered or dismissed. Question prompts are also detected by the default terminal-screen heuristics. (#51, thanks @mspiegel31)
@@ -420,7 +420,7 @@
### Added
- Added first-run onboarding flow that lets you choose notification preferences (sound and toast) on startup.
- Added optional visual toast notifications in the top-right corner for background workspace events (completion and attention-needed alerts).
-- Added configurable keybindings for all navigate mode actions: new workspace, rename workspace, close workspace, resize mode, and toggle sidebar. See `CONFIGURATION.md` for the full key reference.
+- Added configurable keybindings for all navigate mode actions: new workspace, rename workspace, close workspace, resize mode, and toggle sidebar. See the [configuration docs](https://herdr.dev/docs/configuration/) for the full key reference.
- Added configuration validation with startup diagnostics. Invalid key combinations or duplicate bindings now fall back to safe defaults with a visible warning.
### Changed
@@ -431,7 +431,7 @@
- Keybinding parser now accepts special keys (`enter`, `esc`, `tab`, `backspace`, `space`) and function keys (`f1`–`f12`).
### Documentation
-- Split configuration reference into a dedicated `CONFIGURATION.md` file with full keybinding documentation and config diagnostics explanation.
+- Split configuration reference into dedicated configuration docs with full keybinding documentation and config diagnostics explanation.
## [0.1.1] - 2026-03-28
diff --git a/CONFIGURATION.md b/CONFIGURATION.md
deleted file mode 100644
index e988e72e..00000000
--- a/CONFIGURATION.md
+++ /dev/null
@@ -1,486 +0,0 @@
-# configuration
-
-herdr reads config from:
-
-```text
-~/.config/herdr/config.toml
-```
-
-Named sessions share this config file. Sessions are runtime/socket namespaces, not workspace replacements; per-session sockets and persistent runtime state are separate:
-
-```text
-~/.config/herdr/session.json
-~/.config/herdr/sessions/<name>/session.json
-```
-
-Use `herdr session list`, `herdr session attach <name>`, `herdr session stop <name>`, and `herdr session delete <name>` to inspect and manage named session namespaces. Add `--json` to session commands when scripts need machine-readable output.
-
-In default persistence mode, quitting the UI detaches the current client. Use `herdr server stop` to stop the shared background server.
-
-print the full default config with:
-
-```bash
-herdr --default-config
-```
-
-if a config value is invalid, or two navigate actions use the same keybinding, herdr falls back to a safe default and shows a startup warning in the UI.
-
-## live reload
-
-After editing `config.toml`, reload the running app without restarting the persistent server:
-
-```bash
-herdr server reload-config
-```
-
-You can also use the global menu inside herdr and choose `reload config`.
-
-Reload is server-owned. In persistent mode the CLI sends a request to the running server, and the server reads, parses, validates, and applies `config.toml`.
-
-Reloadable now:
-- keybindings and prefix
-- theme, custom theme colors, and legacy `ui.accent`
-- `ui.confirm_close`
-- `ui.agent_panel_scope`
-- `ui.toast.delivery`
-- server-side `ui.sound` policy; attached thin clients refresh local sound config after a successful sound-policy change
-- `experimental.kitty_graphics`
-- `advanced.scrollback_limit_bytes` for panes created after reload
-- `ui.sidebar_width` as the default width; current width updates only while it is still config-owned
-
-Startup-only or special-case:
-- `onboarding` does not reopen onboarding during reload
-- `experimental.allow_nested` is checked before launch and needs a restart
-- existing pane scrollback buffers are not resized during reload
-- terminal notifications and sounds are client-local side effects and are sent to the foreground attached client
-
-If the TOML cannot be read or parsed, reload applies nothing and keeps the current running state. If keybindings are invalid, herdr keeps the current keybindings while applying other valid reloadable settings where possible.
-
-## onboarding
-
-```toml
-onboarding = true
-```
-
-| option | default | description |
-|--------|---------|-------------|
-| `onboarding` | unset | show first-run notification setup; set `false` after choosing |
-
-notes:
-- missing `onboarding` currently behaves like `true`
-- set `onboarding = true` to force the setup screen again for testing
-- continuing from onboarding writes `onboarding = false` and opens the normal settings UI
-
-## keybindings
-
-keybindings live under `[keys]`.
-
-supported syntax:
-- plain keys: `n`, `x`, `-`, `` ` ``
-- modifiers: `ctrl+b`, `shift+n`, `alt+x`, `cmd+x`, `super+x`
-- special keys: `enter`, `esc`, `tab`, `backspace`, `left`, `right`, `up`, `down`
-- function keys: `f1`, `f12`
-- uppercase letters also imply shift: `D` works like `shift+d`
-
-notes:
-- most reliable bindings are plain keys, `ctrl+letter`, `esc`/`tab`/`enter`, and function keys
-- `alt+...`, `cmd`/`super`, and punctuation-with-modifiers may vary depending on terminal/tmux setup
-- bindings marked `unset` in the key reference are supported actions with no default key assigned
-- for navigate-mode actions, duplicate keybindings are treated as config errors; later conflicting bindings fall back to defaults
-
-example:
-
-```toml
-[keys]
-prefix = "ctrl+b"
-new_workspace = "n"
-rename_workspace = "shift+n"
-close_workspace = "X"
-reload_config = "" # optional, unset by default
-open_notification_target = "" # optional, unset by default
-new_tab = "c"
-split_vertical = "d"
-split_horizontal = "D"
-close_pane = "x"
-rename_pane = "" # optional, unset by default
-edit_scrollback = "" # optional, opens focused pane scrollback in $EDITOR
-zoom = "f" # legacy alias: fullscreen
-resize_mode = "r"
-toggle_sidebar = "b"
-previous_workspace = "ctrl+alt+["
-next_workspace = "ctrl+alt+]"
-previous_agent = "ctrl+["
-next_agent = "ctrl+]"
-previous_tab = "alt+["
-next_tab = "alt+]"
-focus_pane_left = "alt+h"
-focus_pane_down = "alt+j"
-focus_pane_up = "alt+k"
-focus_pane_right = "alt+l"
-
-[keys.indexed]
-tabs = "" # optional; e.g. "ctrl" makes ctrl+1..9 switch tabs
-workspaces = "" # optional; e.g. "ctrl+shift" makes ctrl+shift+1..9 switch workspaces
-agents = "" # optional; follows visible agent panel order
-```
-
-### key reference
-
-| key | default | action |
-|-----|---------|--------|
-| `prefix` | `ctrl+b` | enter or leave navigate mode |
-| `new_workspace` | `n` | create a new workspace |
-| `rename_workspace` | `shift+n` | rename selected workspace |
-| `close_workspace` | `shift+d` | close selected workspace |
-| `detach` | unset | optional explicit detach shortcut in the persistent session |
-| `reload_config` | unset | reload `config.toml` in the running app/server |
-| `open_notification_target` | unset | jump to the currently visible notification target |
-| `previous_workspace` | unset | switch to the previous workspace directly from terminal mode |
-| `next_workspace` | unset | switch to the next workspace directly from terminal mode |
-| `previous_agent` | unset | focus the previous agent shown in the sidebar agent list |
-| `next_agent` | unset | focus the next agent shown in the sidebar agent list |
-| `new_tab` | `c` | create a new tab |
-| `rename_tab` | unset | rename the active tab |
-| `previous_tab` | unset | switch to the previous tab directly from terminal mode |
-| `next_tab` | unset | switch to the next tab directly from terminal mode |
-| `close_tab` | unset | close the active tab |
-| `focus_pane_left` | unset | focus the pane to the left directly from terminal mode |
-| `focus_pane_down` | unset | focus the pane below directly from terminal mode |
-| `focus_pane_up` | unset | focus the pane above directly from terminal mode |
-| `focus_pane_right` | unset | focus the pane to the right directly from terminal mode |
-| `split_vertical` | `v` | split pane vertically (side by side) |
-| `split_horizontal` | `-` | split pane horizontally (stacked) |
-| `close_pane` | `x` | close focused pane |
-| `rename_pane` | unset | rename the focused pane |
-| `edit_scrollback` | unset | open the focused pane's retained scrollback in `$EDITOR` inside a temporary zoomed pane |
-| `zoom` | `f` | zoom focused pane; legacy alias: `fullscreen` |
-| `resize_mode` | `r` | enter or leave resize mode |
-| `toggle_sidebar` | `b` | collapse or expand the sidebar |
-
-`edit_scrollback` writes the focused pane's retained plain-text scrollback to a temporary file, opens `${EDITOR:-vi}` on that file in a temporary zoomed pane, then removes the file when the editor exits.
-
-### indexed keybindings
-
-Use `[keys.indexed]` to bind number keys `1` through `9` as positional shortcuts. Each value is a modifier combo only. Empty values disable that shortcut family.
-
-```toml
-[keys.indexed]
-tabs = ""
-workspaces = ""
-agents = ""
-```
-
-| key | default | action |
-|-----|---------|--------|
-| `tabs` | unset | switch to tab 1-9 in the active workspace, left to right |
-| `workspaces` | unset | switch to workspace 1-9 in sidebar order, top to bottom |
-| `agents` | unset | focus agent row 1-9 in the visible agent panel order |
-
-### custom command keybindings
-
-Use `[[keys.command]]` to bind a prefix-mode key to a command. Press the prefix key, then the configured key.
-
-```toml
-[[keys.command]]
-key = "g"
-type = "pane"
-command = "lazygit"
-```
-
-`type` is optional and defaults to `shell`.
-
-| type | behavior |
-|------|----------|
-| `shell` | run the command detached in the background |
-| `pane` | open a temporary zoomed pane, run the command there, then close the pane when the command exits |
-
-Commands run through `/bin/sh -lc`. Herdr sets the command working directory to the active pane cwd when available and provides context through environment variables:
-
-| variable | value |
-|----------|-------|
-| `HERDR_SOCKET_PATH` | active herdr socket path |
-| `HERDR_BIN_PATH` | current herdr binary path |
-| `HERDR_ACTIVE_WORKSPACE_ID` | active workspace id |
-| `HERDR_ACTIVE_TAB_ID` | active tab id |
-| `HERDR_ACTIVE_PANE_ID` | focused pane id |
-| `HERDR_ACTIVE_PANE_CWD` | focused pane cwd |
-
-Example detached helper:
-
-```toml
-[[keys.command]]
-key = "shift+g"
-type = "shell"
-command = "notify-send herdr 'custom command ran'"
-```
-
-## theme
-
-herdr ships with 17 built-in color themes. set one in config:
-
-```toml
-[theme]
-name = "tokyo-night"
-```
-
-### built-in themes
-
-| name | description |
-|------|-------------|
-| `catppuccin` | soft pastel mocha palette (default) |
-| `catppuccin-latte` | light catppuccin palette |
-| `tokyo-night` | blue-purple aesthetic |
-| `tokyo-night-day` | light tokyo night palette |
-| `dracula` | purple/pink/green classic |
-| `nord` | frosty scandinavian blues |
-| `gruvbox` | warm retro browns/oranges |
-| `gruvbox-light` | light gruvbox palette |
-| `one-dark` | atom's beloved dark palette |
-| `one-light` | atom's light palette |
-| `solarized` | ethan schoonover's classic dark palette |
-| `solarized-light` | ethan schoonover's classic light palette |
-| `kanagawa` | hokusai-inspired |
-| `kanagawa-lotus` | light kanagawa palette |
-| `rose-pine` | muted, elegant |
-| `rose-pine-dawn` | light rosé pine palette |
-| `vesper` | high-contrast monochrome with peach and mint accents |
-
-theme names are flexible: `tokyo-night`, `tokyonight`, and `tokyo_night` all work.
-
-### custom overrides
-
-override individual color tokens on top of any base theme:
-
-```toml
-[theme]
-name = "dracula"
-
-[theme.custom]
-panel_bg = "reset"
-accent = "#f5c2e7"
-red = "rgb(255, 85, 85)"
-green = "#a6e3a1"
-```
-
-all tokens are optional — only set what you want to change.
-
-### available tokens
-
-| token | used for |
-|-------|----------|
-| `accent` | highlights, active borders, navigation UI |
-| `panel_bg` | floating panel, tab bar, and overlay background |
-| `surface0` | selected item background |
-| `surface1` | hover/active backgrounds |
-| `surface_dim` | active workspace background, separators |
-| `overlay0` | muted text, secondary info |
-| `overlay1` | slightly brighter secondary text |
-| `text` | primary text |
-| `subtext0` | workspace names, dimmed labels |
-| `mauve` | git branch names, special labels |
-| `green` | idle/done states |
-| `yellow` | busy/running states |
-| `red` | waiting/needs attention states |
-| `blue` | unseen notifications |
-| `teal` | done notification accents |
-| `peach` | interrupted/warning states |
-
-tokens accept the same color formats as `accent`: hex (`#rrggbb`), named colors, or `rgb(r,g,b)`.
-
-for `panel_bg`, you can also use `reset`, `default`, `none`, or `transparent` to stop herdr from painting an opaque panel background and instead use the host terminal's default background.
-
-## ui
-
-```toml
-[ui]
-sidebar_width = 26
-mouse_capture = true
-confirm_close = true
-prompt_new_tab_name = true
-show_agent_labels_on_pane_borders = false
-agent_panel_scope = "all"
-accent = "cyan"
-```
-
-### options
-
-| option | default | description |
-|--------|---------|-------------|
-| `sidebar_width` | `26` | base sidebar width before auto-scaling |
-| `mouse_capture` | `true` | capture mouse input for Herdr's mouse UI; set false to let the terminal handle normal clicks while still forwarding mouse to pane apps that request it |
-| `confirm_close` | `true` | ask before closing a workspace |
-| `prompt_new_tab_name` | `true` | ask for a tab name before creating a new tab; set false to create tabs immediately with generated names |
-| `show_agent_labels_on_pane_borders` | `false` | show detected/reported agent labels in split pane borders when no manual pane name is set |
-| `agent_panel_scope` | `all` | sidebar agent list scope: `current` or `all` |
-| `accent` | `cyan` | highlight and border color |
-
-Changing the agent panel scope from the sidebar writes `agent_panel_scope` to config so it survives session resets and upgrades.
-
-`accent` accepts:
-- named colors like `cyan`, `blue`, `magenta`
-- hex like `#89b4fa`
-- rgb like `rgb(137,180,250)`
-
-## toast notifications
-
-```toml
-[ui.toast]
-delivery = "off"
-```
-
-### options
-
-| option | default | description |
-|--------|---------|-------------|
-| `ui.toast.delivery` | `off` | where background popup notifications should appear |
-
-available values:
-- `off` — disable popup notifications
-- `herdr` — show top-right in-app toasts
-- `terminal` — ask the outer terminal to show a desktop notification. some terminals suppress foreground notifications, including ghostty on macos.
-- `system` — ask the os notification service directly. on macos, herdr uses `terminal-notifier` when available and falls back to built-in `osascript`. on linux, `system` requires `notify-send`.
-
-### macos system notifications
-
-for best macos support, install `terminal-notifier`:
-
-```sh
-brew install terminal-notifier
-```
-
-when `terminal-notifier` is installed, herdr tries to focus the hosting terminal when a notification is clicked. click-to-return is supported for detected ghostty, iterm2, wezterm, kitty, alacritty, and terminal.app sessions.
-
-without `terminal-notifier`, herdr falls back to built-in `osascript`. this still shows a macos notification, but clicking the notification may focus the apple script runner instead of returning to your terminal.
-
-compatibility note:
-- older configs may still use `ui.toast.enabled = true|false`
-- herdr still reads that legacy key for compatibility
-- if you save toast settings from inside herdr, it rewrites the setting to `ui.toast.delivery`
-
-current behavior:
-- informational only
-- one notification event at a time
-- shown for background agent events like `needs attention` and `finished`
-- suppression is tab-aware: the active tab stays quiet, but background tabs in the same workspace can still notify
-- `terminal` delivery is best-effort and depends on terminal support
-- `system` delivery is best-effort and depends on the os helper being available
-- macos `system` delivery prefers `terminal-notifier` when present and falls back to `osascript`
-- currently targets terminals such as ghostty, kitty, iterm2, and wezterm
-- inside tmux, herdr wraps notification escapes with tmux passthrough
-
-## sound
-
-```toml
-[ui.sound]
-enabled = true
-
-[ui.sound.agents]
-claude = "default"
-droid = "off"
-```
-
-### options
-
-| option | default | description |
-|--------|---------|-------------|
-| `ui.sound.enabled` | `true` | enable background agent sounds |
-
-per-agent values:
-- `default`
-- `on`
-- `off`
-
-available agent keys:
-- `pi`
-- `claude`
-- `codex`
-- `gemini`
-- `cursor`
-- `cline`
-- `open_code`
-- `github_copilot`
-- `kimi`
-- `droid`
-- `amp`
-- `grok`
-
-## experimental
-
-```toml
-[experimental]
-allow_nested = false
-kitty_graphics = false
-```
-
-### options
-
-| option | default | description |
-|--------|---------|-------------|
-| `experimental.allow_nested` | `false` | allow launching herdr from inside a herdr-managed pane |
-| `experimental.kitty_graphics` | `false` | enable experimental local Kitty graphics rendering for attached clients |
-
-### nested launches
-
-By default, herdr blocks nested launches when `HERDR_ENV=1` is already present.
-
-Set `allow_nested = true` only for debugging or intentionally nested setups.
-
-### Kitty graphics
-
-`kitty_graphics` enables experimental local Kitty graphics rendering for attached clients.
-
-It requires a Kitty graphics-compatible outer terminal.
-
-Known limitation: resizing the terminal window or changing the terminal font while images are visible can leave existing images misplaced or stale. Restart the pane app or clear and redraw the image output after changing size or font. Please report any findings so this experimental path can improve.
-
-## advanced
-
-```toml
-[advanced]
-scrollback_limit_bytes = 10000000
-```
-
-### options
-
-| option | default | description |
-|--------|---------|-------------|
-| `advanced.scrollback_limit_bytes` | `10000000` | maximum scrollback buffer size in bytes retained per pane terminal |
-
-### scrollback
-
-`scrollback_limit_bytes` limits retained terminal scrollback per pane.
-
-The default matches Ghostty's `scrollback-limit` value.
-
-Set `scrollback_limit_bytes = 0` to disable pane scrollback entirely.
-
-The legacy `scrollback_lines` key is still accepted inside `[advanced]`, but it uses the same byte-based value.
-
-## environment variables
-
-| variable | description |
-|----------|-------------|
-| `HERDR_LOG` | log level filter (default: `herdr=info`) |
-
-## logs
-
-herdr writes local file logs under:
-
-```text
-~/.config/herdr/
-```
-
-common files:
-
-```text
-~/.config/herdr/herdr.log
-~/.config/herdr/herdr-client.log
-~/.config/herdr/herdr-server.log
-```
-
-notes:
-- `herdr.log` is used by monolithic `--no-session` mode and some top-level startup paths
-- persistent session mode mainly uses `herdr-client.log` and `herdr-server.log`
-- logs rotate automatically by size and keep a few older files as `.1`, `.2`, and so on
-- default logs are metadata-focused and are intended to be shareable for issue diagnosis
-- `HERDR_LOG` can increase verbosity when you need a local repro or deeper debugging
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 8b4d6d9d..a79aa29b 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -50,16 +50,9 @@ If you plan to implement the change yourself, say that directly in the issue and
## Documentation for unreleased changes
-The root docs describe the latest released version of herdr. Do not update root `README.md`, `CONFIGURATION.md`, `INTEGRATIONS.md`, `SOCKET_API.md`, or `CHANGELOG.md` for normal PRs.
+The root `README.md`, root `CHANGELOG.md`, and website docs describe the latest released version of herdr. Do not update root `README.md`, root `CHANGELOG.md`, or `website/src/content/docs/` for normal PRs.
-If your PR changes user-facing behavior, update the matching next-release docs under `docs/next/` instead.
-
-Examples:
-
-- config changes: update `docs/next/CONFIGURATION.md`
-- integration changes: update `docs/next/INTEGRATIONS.md`
-- socket/API changes: update `docs/next/SOCKET_API.md`
-- workflow or usage changes: update `docs/next/README.md`
+If your PR changes user-facing behavior, mention the needed public-doc update in the PR. Update `docs/next/README.md` only when the root README needs to change for the next release.
You do not need to edit the changelog for normal PRs. Maintainers prepare `docs/next/CHANGELOG.md` during release review.
diff --git a/INTEGRATIONS.md b/INTEGRATIONS.md
deleted file mode 100644
index 23b41ab8..00000000
--- a/INTEGRATIONS.md
+++ /dev/null
@@ -1,240 +0,0 @@
-# integrations
-
-herdr works without any hook or plugin setup.
-
-out of the box, it detects supported agents automatically by combining foreground process detection with screen heuristics. that is enough to give you workspace awareness with zero configuration.
-
-when an agent exposes hooks or plugins, the more robust path is to forward semantic state to herdr over the local socket api. the built-in integrations in this document do exactly that.
-
-if you want to inspect the exact files herdr installs, they are versioned in this repo:
-
-- [pi extension](./src/integration/assets/pi/herdr-agent-state.ts)
-- [claude code hook](./src/integration/assets/claude/herdr-agent-state.sh)
-- [codex hook](./src/integration/assets/codex/herdr-agent-state.sh)
-- [opencode plugin](./src/integration/assets/opencode/herdr-agent-state.js)
-
-## how herdr uses integrations
-
-herdr uses a hybrid model:
-
-- **process detection** owns pane identity, liveness, and "the process is gone"
-- **agent integrations** report semantic state like `working`, `blocked`, and `idle` over the local socket api when the tool exposes those events
-- **screen heuristics** remain the fallback for gaps, unsupported tools, or incomplete hook surfaces
-
-that means hooks/plugins do **not** become the source of truth for pane ownership. they enrich state reporting; they do not replace process detection.
-
-## install commands
-
-```bash
-herdr integration install pi
-herdr integration install claude
-herdr integration install codex
-herdr integration install opencode
-```
-
-## uninstall commands
-
-```bash
-herdr integration uninstall pi
-herdr integration uninstall claude
-herdr integration uninstall codex
-herdr integration uninstall opencode
-```
-
-## pi
-
-install:
-
-```bash
-herdr integration install pi
-```
-
-this writes the bundled pi extension to:
-
-```text
-~/.pi/agent/extensions/herdr-agent-state.ts
-```
-
-if `PI_CODING_AGENT_DIR` is set, herdr uses that agent directory instead and writes to:
-
-```text
-$PI_CODING_AGENT_DIR/extensions/herdr-agent-state.ts
-```
-
-`~` is expanded in `PI_CODING_AGENT_DIR`.
-
-pi is the cleanest integration. it already has an authoritative hook model, so herdr can get direct state reports over the socket api without guessing as much from the terminal.
-
-bundled source: [`src/integration/assets/pi/herdr-agent-state.ts`](./src/integration/assets/pi/herdr-agent-state.ts)
-
-uninstall:
-
-```bash
-herdr integration uninstall pi
-```
-
-this removes the same extension path herdr would install, using `PI_CODING_AGENT_DIR` when it is set.
-
-## claude code
-
-install:
-
-```bash
-herdr integration install claude
-```
-
-this:
-
-- writes the hook script to `~/.claude/hooks/herdr-agent-state.sh`
-- updates `~/.claude/settings.json`
-
-if `CLAUDE_CONFIG_DIR` is set, herdr uses that directory instead, for example `$CLAUDE_CONFIG_DIR/hooks/herdr-agent-state.sh` and `$CLAUDE_CONFIG_DIR/settings.json`. `~` is expanded in `CLAUDE_CONFIG_DIR`.
-
-bundled source: [`src/integration/assets/claude/herdr-agent-state.sh`](./src/integration/assets/claude/herdr-agent-state.sh)
-
-current hook mapping:
-
-- `UserPromptSubmit` → `working`
-- `PreToolUse` → `working`
-- `PermissionRequest` → `blocked`
-- `PostToolUse` → `working`
-- `PostToolUseFailure` → `working`
-- `SubagentStop` → `working`
-- `Stop` → `idle`
-- `SessionEnd` → `release`
-
-notes:
-
-- claude code hooks also run inside subagents. herdr treats subagent `working` and `blocked` reports as real pane state.
-- subagent stop/release events are converted to `working` by the bundled hook script so a completed subagent does not make the parent claude pane look idle.
-- `PostToolUse` and `PostToolUseFailure` move the pane back to `working` after a permissioned tool call resolves.
-- some non-claude tools, including grok cli, may load claude settings or plugins. herdr ignores conflicting known-agent hook labels once native foreground-process detection identifies a different known agent.
-
-uninstall:
-
-```bash
-herdr integration uninstall claude
-```
-
-this removes the same hook path and settings entries herdr would install, using `CLAUDE_CONFIG_DIR` when it is set.
-
-## codex
-
-install:
-
-```bash
-herdr integration install codex
-```
-
-this:
-
-- writes the hook script to `~/.codex/herdr-agent-state.sh`
-- updates `~/.codex/hooks.json`
-- ensures `hooks = true` under `[features]` in `~/.codex/config.toml`
-- migrates the deprecated top-level `[features] codex_hooks = true` setting to `hooks = true`
-
-if `CODEX_HOME` is set, herdr uses that directory instead, for example `$CODEX_HOME/herdr-agent-state.sh`, `$CODEX_HOME/hooks.json`, and `$CODEX_HOME/config.toml`. `~` is expanded in `CODEX_HOME`.
-
-bundled source: [`src/integration/assets/codex/herdr-agent-state.sh`](./src/integration/assets/codex/herdr-agent-state.sh)
-
-current hook mapping:
-
-- `SessionStart` → `idle`
-- `UserPromptSubmit` → `working`
-- `PreToolUse` → `working`
-- `Stop` → `idle`
-
-notes:
-
-- codex does **not** currently expose a permission-specific hook like claude or opencode, so `blocked` still depends on herdr's normal heuristics.
-- codex currently renders hook lifecycle messages in its own tui, for example `Running SessionStart hook` and `SessionStart hook (completed)`.
-- that noise is an upstream codex limitation, not a herdr-specific issue.
-- codex has a `suppressOutput` field in its hook output schema, but it is currently not effective for suppressing those tui lifecycle lines.
-
-uninstall:
-
-```bash
-herdr integration uninstall codex
-```
-
-this:
-
-- removes the same hook path herdr would install, using `CODEX_HOME` when it is set
-- removes herdr-owned hook entries from the matching `hooks.json`
-- intentionally leaves the matching `config.toml` alone
-
-that last point is deliberate: herdr does **not** try to guess whether `hooks = true` is still needed for some other codex hook setup.
-
-## opencode
-
-install:
-
-```bash
-herdr integration install opencode
-```
-
-this writes the bundled plugin to:
-
-```text
-~/.config/opencode/plugins/herdr-agent-state.js
-```
-
-bundled source: [`src/integration/assets/opencode/herdr-agent-state.js`](./src/integration/assets/opencode/herdr-agent-state.js)
-
-current plugin mapping:
-
-- `permission.asked` → `blocked`
-- `permission.replied: once|always` → `working`
-- `permission.replied: reject` → `idle`
-- `question.asked` → `blocked`
-- `question.replied` → `working`
-- `question.rejected` → `idle`
-- `session.status: busy|retry` → `working`
-- `session.status: idle` → `idle`
-- `session.idle` → `idle`
-
-notes:
-
-- opencode has the richest event surface of the currently supported integrations.
-- herdr intentionally does **not** guess that `session.deleted` means process exit. process detection still owns liveness and pane identity.
-
-uninstall:
-
-```bash
-herdr integration uninstall opencode
-```
-
-this removes:
-
-```text
-~/.config/opencode/plugins/herdr-agent-state.js
-```
-
-## grok cli
-
-herdr does not currently install a grok hook or plugin.
-
-grok is heuristic-only in herdr. herdr detects `grok` and `grok-build` from the foreground process and uses screen heuristics for `working`, `blocked`, and `idle` states.
-
-grok cli may load claude settings or plugins as a compatibility feature, including hooks from `~/.claude/settings.json`. if that causes the claude herdr hook to run in a grok pane, herdr ignores the conflicting `claude` hook label once native foreground-process detection identifies the pane as `grok`.
-
-## amp
-
-herdr does not currently install an Amp plugin.
-
-Amp's public plugin API exposes lifecycle and tool-call hooks, but not passive permission/request-blocked events. A Herdr Amp plugin that only reports `idle` and `working` would take hook authority for the pane and mask Herdr's existing screen heuristics for Amp `blocked` states. Until Amp exposes permission state as an observable plugin event, Amp remains heuristic-only in Herdr.
-
-## known limitations
-
-- these integrations only activate inside herdr-managed panes.
-- if an agent has an incomplete hook surface, herdr falls back to process detection and screen heuristics rather than inventing lease or ttl behavior.
-- codex currently shows hook lifecycle chatter in its own tui until upstream adds a real silent mode.
-
-## troubleshooting
-
-if an install command succeeds but you do not see improved state reporting:
-
-1. make sure you launched the agent inside a herdr pane
-2. restart the agent session so it picks up the new hook/plugin config
-3. verify the expected config file was written to the path above
-4. remember that unsupported transitions still fall back to herdr's built-in heuristics
diff --git a/README.md b/README.md
index ab609a05..d22d4051 100644
--- a/README.md
+++ b/README.md
@@ -6,7 +6,7 @@
</p>
<p align="center">
- <a href="https://herdr.dev">herdr.dev</a> · <a href="#install">install</a> · <a href="#quick-start">quick start</a> · <a href="#supported-agents">supported agents</a> · <a href="./INTEGRATIONS.md">integrations</a> · <a href="./CONFIGURATION.md">configuration</a> · <a href="./SOCKET_API.md">socket api</a>
+ <a href="https://herdr.dev">herdr.dev</a> · <a href="#install">install</a> · <a href="#quick-start">quick start</a> · <a href="#supported-agents">supported agents</a> · <a href="https://herdr.dev/docs/integrations/">integrations</a> · <a href="https://herdr.dev/docs/configuration/">configuration</a> · <a href="https://herdr.dev/docs/socket-api/">socket api</a>
</p>
---
@@ -163,7 +163,7 @@ herdr pane read 1-2 --source recent --lines 50
herdr pane read 1-2 --source visible --ansi
```
-full reference: [`SOCKET_API.md`](./SOCKET_API.md) and [`SKILL.md`](./SKILL.md).
+full reference: [socket api](https://herdr.dev/docs/socket-api/) and [`SKILL.md`](./SKILL.md).
## supported agents
@@ -181,7 +181,7 @@ automatic detection works out of the box. process name matching plus terminal ou
detected but not fully tested: gemini cli, cursor agent, cline, kimi, github copilot cli.
-for agents outside the built-in list, herdr still works as a terminal multiplexer with workspaces, panes, and tiling. custom integrations can report agent labels over the socket api. see [`SOCKET_API.md`](./SOCKET_API.md).
+for agents outside the built-in list, herdr still works as a terminal multiplexer with workspaces, panes, and tiling. custom integrations can report agent labels over the socket api. see the [socket api docs](https://herdr.dev/docs/socket-api/).
### direct integrations
@@ -194,7 +194,7 @@ herdr integration install codex
herdr integration install opencode
```
-see [`INTEGRATIONS.md`](./INTEGRATIONS.md) for setup details.
+see the [integrations docs](https://herdr.dev/docs/integrations/) for setup details.
## keybindings
@@ -224,7 +224,7 @@ type = "pane" # "shell" or "pane"
command = "lazygit"
```
-mouse is supported throughout. full reference: [`CONFIGURATION.md`](./CONFIGURATION.md).
+mouse is supported throughout. full reference: [configuration docs](https://herdr.dev/docs/configuration/).
## configuration
@@ -234,7 +234,7 @@ config file: `~/.config/herdr/config.toml`
herdr --default-config # print full default config
```
-in-app settings screen for theme, sound, and toast preferences. full reference: [`CONFIGURATION.md`](./CONFIGURATION.md).
+in-app settings screen for theme, sound, and toast preferences. full reference: [configuration docs](https://herdr.dev/docs/configuration/).
## logs
@@ -258,14 +258,14 @@ use a higher log level only when needed:
HERDR_LOG=herdr=debug herdr
```
-full logging and environment variable details: [`CONFIGURATION.md`](./CONFIGURATION.md).
+full logging and environment variable details: [configuration docs](https://herdr.dev/docs/configuration/).
## docs
-- [`CONFIGURATION.md`](./CONFIGURATION.md) — keybindings, themes, notifications, environment variables
-- [`INTEGRATIONS.md`](./INTEGRATIONS.md) — pi, claude code, codex, opencode integrations
+- [configuration](https://herdr.dev/docs/configuration/) — keybindings, themes, notifications, environment variables
+- [integrations](https://herdr.dev/docs/integrations/) — pi, claude code, codex, opencode integrations
- [`SKILL.md`](./SKILL.md) — reusable agent skill
-- [`SOCKET_API.md`](./SOCKET_API.md) — socket protocol and cli reference
+- [socket api](https://herdr.dev/docs/socket-api/) — socket protocol and cli reference
## agent instructions
diff --git a/SKILL.md b/SKILL.md
index 8af56cd1..29d0278a 100644
--- a/SKILL.md
+++ b/SKILL.md
@@ -19,7 +19,7 @@ this means you can:
the `herdr` binary is available in your PATH. its workspace, tab, pane, and wait commands talk to the running herdr instance over a local unix socket.
-if you need the raw protocol or full api reference, read [`SOCKET_API.md`](./SOCKET_API.md).
+if you need the raw protocol or full api reference, read the [socket api docs](https://herdr.dev/docs/socket-api/).
## concepts
diff --git a/SOCKET_API.md b/SOCKET_API.md
deleted file mode 100644
index 7ffb276e..00000000
--- a/SOCKET_API.md
+++ /dev/null
@@ -1,1112 +0,0 @@
-# herdr socket api
-
-herdr exposes a local unix socket api for scripts, tools, and coding agents that want to control a running herdr instance or subscribe to events.
-
-if you are teaching an agent that is already running inside herdr, start with [`SKILL.md`](./SKILL.md). use this document when you want the direct protocol, or when you want the cli wrapper reference for the commands that sit on top of it.
-
-## choose your integration layer
-
-there are three practical ways to integrate with herdr:
-
-- **agent skill** — [`SKILL.md`](./SKILL.md). best when an agent inside herdr just needs to learn the workflow quickly.
-- **cli wrappers** — `herdr server stop`, `herdr workspace ...`, `herdr tab ...`, `herdr agent ...`, `herdr pane ...`, `herdr wait ...`. best for shell scripts and simple orchestration.
-- **raw socket api** — best when you want direct request/response control or long-lived event subscriptions.
-
-these layers are intentionally stacked on top of the same control surface.
-
-important difference: `pane.run` and `wait agent-status` are **cli conveniences**, not raw socket methods.
-
-## transport
-
-- transport: unix domain socket
-- encoding: newline-delimited json
-- request/response: send one json request per line, read one json response per line
-- subscriptions: send `events.subscribe`, receive an ack, then keep the same connection open and continue reading pushed events
-
-named sessions are runtime/socket namespaces, not replacements for herdr workspaces. each named session has its own server sockets and persistent runtime state while config remains global.
-
-socket path resolution order:
-
-1. explicit `herdr --session <name>`:
- `$XDG_CONFIG_HOME/herdr/sessions/<name>/herdr.sock` or `$HOME/.config/herdr/sessions/<name>/herdr.sock`
-2. `HERDR_SOCKET_PATH`
-3. `HERDR_SESSION=<name>`:
- `$XDG_CONFIG_HOME/herdr/sessions/<name>/herdr.sock` or `$HOME/.config/herdr/sessions/<name>/herdr.sock`
-4. default session path:
- `$XDG_CONFIG_HOME/herdr/herdr.sock` or `$HOME/.config/herdr/herdr.sock`
-
-this means `HERDR_SOCKET_PATH` remains an exact low-level socket override, but an explicit cli `--session <name>` still wins when a command runs inside a pane that inherited `HERDR_SOCKET_PATH`.
-
-session names may contain ASCII letters, numbers, `.`, `_`, and `-`. `default` is reserved for the default session. use `herdr session list`, `herdr session attach <name>`, `herdr session stop <name>`, and `herdr session delete <name>` to inspect and manage session namespaces. session commands print human-readable output by default; pass `--json` for machine-readable output. `session delete` refuses running sessions and does not delete the default session.
-
-## request and response envelopes
-
-all socket requests use this envelope:
-
-```json
-{
- "id": "req_1",
- "method": "ping",
- "params": {}
-}
-```
-
-successful responses look like:
-
-```json
-{
- "id": "req_1",
- "result": {
- "type": "pong",
- "version": "0.1.2",
- "protocol": 2
- }
-}
-```
-
-errors look like:
-
-```json
-{
- "id": "req_1",
- "error": {
- "code": "pane_not_found",
- "message": "pane 1-99 not found"
- }
-}
-```
-
-## ids and numbering
-
-workspace ids are opaque, stable ids like:
-
-- `w64e95948145ed1`
-- `w64e95948146a82`
-
-pane ids are workspace-scoped and stable across workspace reorder:
-
-- `w64e95948145ed1-1`
-- `w64e95948145ed1-2`
-- `w64e95948146a82-1`
-
-that means:
-
-- workspace id = stable workspace identity
-- pane number = compact pane number within that workspace
-
-workspace ids are durable for the life of the workspace and survive display reordering. pane numbers are still compact public numbers, so if a pane closes, higher pane numbers in that same workspace compact down.
-
-tabs are first-class socket api objects now.
-
-- tab ids look like `w64e95948145ed1:1`, `w64e95948145ed1:2`
-- workspace id = stable workspace identity
-- tab number = tab number within that workspace
-- pane ids still stay workspace-scoped like `w64e95948145ed1-2` rather than becoming `workspace-tab-pane` triples
-
-for backward compatibility, requests also accept the older positional forms like `1`, `1:2`, and `1-2` as shorthand for the current session order. responses use the stable ids.
-
-## core objects
-
-`workspace_info` responses contain objects like:
-
-```json
-{
- "workspace_id": "w64e95948145ed1",
- "number": 1,
- "label": "herdr",
- "focused": true,
- "pane_count": 1,
- "tab_count": 1,
- "active_tab_id": "w64e95948145ed1:1",
- "agent_status": "unknown"
-}
-```
-
-`tab_info` responses contain objects like:
-
-```json
-{
- "tab_id": "w64e95948145ed1:1",
- "workspace_id": "w64e95948145ed1",
- "number": 1,
- "label": "1",
- "focused": true,
- "pane_count": 1,
- "agent_status": "unknown"
-}
-```
-
-`pane_info` responses contain objects like:
-
-```json
-{
- "pane_id": "w64e95948145ed1-1",
- "terminal_id": "term_64e95948145ed1",
- "workspace_id": "w64e95948145ed1",
- "tab_id": "w64e95948145ed1:1",
- "focused": true,
- "cwd": "/home/can/Projects/herdr",
- "label": "reviewer",
- "agent": "pi",
- "agent_status": "working",
- "custom_status": "indexing",
- "revision": 0
-}
-```
-
-`terminal_id` is an opaque terminal identity. during the pane-backed transition each pane has one terminal id, but clients should not derive it from the pane id.
-
-`label` is an optional manual pane name set through `pane.rename`.
-
-`agent_info` responses contain a terminal-facing view of a live agent terminal:
-
-```json
-{
- "terminal_id": "term_64e95948145ed1",
- "name": "reviewer",
- "agent": "pi",
- "agent_status": "working",
- "workspace_id": "w64e95948145ed1",
- "tab_id": "w64e95948145ed1:1",
- "pane_id": "w64e95948145ed1-1",
- "focused": true,
- "cwd": "/home/can/Projects/herdr",
- "revision": 0
-}
-```
-
-`name` is an optional unique agent alias set by `agent.start` or `agent.rename`. it is separate from the pane `label`: `pane.rename` names a terminal/pane for UI purposes, while `agent.rename` declares or changes the agent-facing name and rejects duplicate active agent names.
-
-`agent` is an optional display label string.
-
-- when herdr detects a built-in agent, this is that built-in name like `pi` or `claude`
-- when a hook or plugin reports a custom agent through `pane.report_agent`, this can be any non-empty label like `hermes`
-- when no agent identity is known, it is omitted
-
-`custom_status` on `pane_info` and agent-status events is an optional hook-owned display label. It is present only while hook authority is active. It does not change `agent_status`, `done` semantics, waits, notifications, or workspace/tab rollup priority.
-
-`pane_read` responses contain objects like:
-
-```json
-{
- "pane_id": "w64e95948145ed1-1",
- "workspace_id": "w64e95948145ed1",
- "tab_id": "w64e95948145ed1:1",
- "source": "recent",
- "format": "text",
- "text": "...",
- "revision": 0,
- "truncated": false
-}
-```
-
-`agent_status` is the public agent field:
-
-- `idle`
-- `working`
-- `blocked`
-- `done`
-- `unknown`
-
-`done` means the agent has finished, but you have not looked at that finished pane yet.
-
-## methods at a glance
-
-| method | purpose | success result type |
-|---|---|---|
-| `ping` | health check / version | `pong` |
-| `server.stop` | gracefully stop the running background server | `ok` |
-| `workspace.list` | list workspaces | `workspace_list` |
-| `workspace.get` | inspect one workspace | `workspace_info` |
-| `workspace.create` | create a workspace | `workspace_info` |
-| `workspace.focus` | focus a workspace | `workspace_info` |
-| `workspace.rename` | rename a workspace | `workspace_info` |
-| `workspace.close` | close a workspace | `ok` |
-| `tab.list` | list tabs, optionally filtered by workspace | `tab_list` |
-| `tab.get` | inspect one tab | `tab_info` |
-| `tab.create` | create a tab in a workspace | `tab_info` |
-| `tab.focus` | focus a tab | `tab_info` |
-| `tab.rename` | rename a tab | `tab_info` |
-| `tab.close` | close a tab | `ok` |
-| `agent.list` | list terminal-backed agents | `agent_list` |
-| `agent.get` | inspect one agent by terminal id, unique agent name, detected agent label, or pane id | `agent_info` |
-| `agent.read` | read output from one agent terminal | `pane_read` |
-| `agent.send` | send literal text to one agent terminal | `ok` |
-| `agent.rename` | set or clear the unique agent name | `agent_info` |
-| `agent.focus` | show the agent terminal in the TUI | `agent_info` |
-| `agent.start` | start a named agent terminal from argv | `agent_started` |
-| `pane.list` | list panes, optionally filtered by workspace | `pane_list` |
-| `pane.get` | inspect one pane | `pane_info` |
-| `pane.rename` | set or clear a manual pane label | `pane_info` |
-| `pane.read` | read pane output | `pane_read` |
-| `pane.split` | split a pane and create a sibling pane | `pane_info` |
-| `pane.send_text` | send literal text without Enter | `ok` |
-| `pane.send_keys` | send keypresses like `Enter` | `ok` |
-| `pane.send_input` | send literal text plus keypresses in order | `ok` |
-| `pane.report_agent` | report hook-authoritative agent label and state for a pane | `ok` |
-| `pane.clear_agent_authority` | clear hook-authoritative agent state for a pane | `ok` |
-| `pane.release_agent` | release a pane from the reported agent back to shell state | `ok` |
-| `pane.close` | close a pane | `ok` |
-| `pane.wait_for_output` | one-shot blocking wait for text | `output_matched` |
-| `events.subscribe` | start a long-lived subscription stream | `subscription_started` ack |
-
-### `server.stop`
-
-request:
-
-```json
-{
- "id": "req_stop",
- "method": "server.stop",
- "params": {}
-}
-```
-
-returns `ok` and asks the running background server to shut down cleanly.
-
-this is the explicit server-level shutdown path for persistence mode. normal in-app quit actions detach the current client instead of sending this request.
-
-## workspace methods
-
-### `workspace.list`
-
-request:
-
-```json
-{
- "id": "req_list",
- "method": "workspace.list",
- "params": {}
-}
-```
-
-returns `workspace_list` with zero or more workspace objects.
-
-### `workspace.get`
-
-params:
-
-```json
-{
- "workspace_id": "1"
-}
-```
-
-returns `workspace_info` for one workspace.
-
-### `workspace.create`
-
-params:
-
-```json
-{
- "cwd": "/home/can/Projects/herdr",
- "focus": true
-}
-```
-
-notes:
-
-- `cwd` is optional
-- if `cwd` is omitted, herdr uses its current working directory and falls back to `/` if needed
-- `focus` is optional in raw socket requests and defaults to `false`
-- the cli wrapper also defaults to no focus; pass `--focus` to switch to the new workspace
-
-example response:
-
-```json
-{
- "id": "req_create",
- "result": {
- "type": "workspace_info",
- "workspace": {
- "workspace_id": "1",
- "number": 1,
- "label": "herdr",
- "focused": true,
- "pane_count": 1,
- "tab_count": 1,
- "active_tab_id": "1:1",
- "agent_status": "unknown"
- }
- }
-}
-```
-
-### `workspace.focus`
-
-params:
-
-```json
-{
- "workspace_id": "1"
-}
-```
-
-returns the focused workspace as `workspace_info`.
-
-### `workspace.rename`
-
-params:
-
-```json
-{
- "workspace_id": "1",
- "label": "api"
-}
-```
-
-returns updated `workspace_info`.
-
-### `workspace.close`
-
-params:
-
-```json
-{
- "workspace_id": "1"
-}
-```
-
-returns:
-
-```json
-{
- "id": "req_close",
- "result": {
- "type": "ok"
- }
-}
-```
-
-## tab methods
-
-### `tab.list`
-
-request with no filter:
-
-```json
-{
- "id": "req_tabs",
- "method": "tab.list",
- "params": {}
-}
-```
-
-request filtered to one workspace:
-
-```json
-{
- "id": "req_tabs_ws",
- "method": "tab.list",
- "params": {
- "workspace_id": "1"
- }
-}
-```
-
-returns `tab_list`.
-
-### `tab.get`
-
-params:
-
-```json
-{
- "tab_id": "1:2"
-}
-```
-
-returns `tab_info`.
-
-### `tab.create`
-
-params:
-
-```json
-{
- "workspace_id": "1",
- "cwd": "/home/can/Projects/herdr",
- "focus": true
-}
-```
-
-notes:
-
-- `workspace_id` is optional and defaults to the active workspace
-- `cwd` is optional; if omitted, herdr uses the focused pane cwd in that workspace when available
-- `focus` is optional in raw socket requests and defaults to `false`
-- the cli wrapper also defaults to no focus; pass `--focus` to switch to the new tab
-
-returns `tab_info` for the new tab.
-
-### `tab.focus`
-
-params:
-
-```json
-{
- "tab_id": "1:2"
-}
-```
-
-returns focused `tab_info`.
-
-### `tab.rename`
-
-params:
-
-```json
-{
- "tab_id": "1:2",
- "label": "logs"
-}
-```
-
-returns updated `tab_info`.
-
-### `tab.close`
-
-params:
-
-```json
-{
- "tab_id": "1:2"
-}
-```
-
-returns `ok`. the last tab in a workspace cannot be closed.
-
-## pane methods
-
-### `pane.list`
-
-request with no filter:
-
-```json
-{
- "id": "req_panes",
- "method": "pane.list",
- "params": {}
-}
-```
-
-request filtered to one workspace:
-
-```json
-{
- "id": "req_panes_ws",
- "method": "pane.list",
- "params": {
- "workspace_id": "1"
- }
-}
-```
-
-returns `pane_list`.
-
-### `pane.get`
-
-params:
-
-```json
-{
- "pane_id": "1-1"
-}
-```
-
-returns `pane_info`.
-
-### `pane.rename`
-
-params:
-
-```json
-{
- "pane_id": "1-1",
- "label": "reviewer"
-}
-```
-
-send `label: null` or omit `label` to clear the manual pane label.
-
-returns `pane_info`.
-
-### `pane.read`
-
-params:
-
-```json
-{
- "pane_id": "1-1",
- "source": "recent",
- "lines": 80,
- "format": "text",
- "strip_ansi": true
-}
-```
-
-notes:
-
-- `source` is required and must be `visible`, `recent`, or `recent_unwrapped`
-- `lines` is optional
-- current implementation defaults to `80` lines when `lines` is omitted and caps reads at `1000`
-- `format` defaults to `text`; use `ansi` for a rendered VT/ANSI snapshot with styles preserved
-- `strip_ansi` defaults to `true` and is kept for compatibility
-
-`source` meanings:
-
-- `visible` — current viewport
-- `recent` — recent scrollback text
-- `recent_unwrapped` — recent scrollback text with soft wraps joined
-
-example response:
-
-```json
-{
- "id": "req_read",
- "result": {
- "type": "pane_read",
- "read": {
- "pane_id": "1-1",
- "workspace_id": "1",
- "tab_id": "1:1",
- "source": "recent",
- "format": "text",
- "text": "...",
- "revision": 0,
- "truncated": false
- }
- }
-}
-```
-
-### `pane.split`
-
-params:
-
-```json
-{
- "target_pane_id": "1-1",
- "direction": "right",
- "focus": true
-}
-```
-
-notes:
-
-- `direction` must be `right` or `down`
-- `cwd` is optional
-- `focus` is optional in raw socket requests and defaults to `false`
-- the cli wrapper also defaults to no focus; pass `--focus` to switch to the new pane
-
-returns `pane_info` for the new pane.
-
-### `pane.send_text`
-
-params:
-
-```json
-{
- "pane_id": "1-1",
- "text": "bun run dev"
-}
-```
-
-this sends literal text only. it does **not** press Enter.
-
-### `pane.send_keys`
-
-params:
-
-```json
-{
- "pane_id": "1-1",
- "keys": ["Enter"]
-}
-```
-
-use this after `pane.send_text` when you want to submit a command.
-
-### `pane.send_input`
-
-params:
-
-```json
-{
- "pane_id": "1-1",
- "text": "bun run dev",
- "keys": ["Enter"]
-}
-```
-
-this sends text plus encoded keypresses in order within one request. when bracketed paste is enabled in the pane, the text portion is sent as a paste payload before the keys. use this when you need `text + Enter` to behave more like a real keypress sequence than `pane.send_text` with a literal trailing `\r`.
-
-`text` and `keys` are both optional, but at least one should usually be present.
-
-### `pane.report_agent`
-
-use this when an agent hook or plugin wants to report a semantic state directly over the socket api.
-
-params:
-
-```json
-{
- "pane_id": "1-1",
- "source": "custom:hermes",
- "agent": "hermes",
- "state": "working",
- "message": "running tools",
- "custom_status": "indexing"
-}
-```
-
-notes:
-
-- `source` is required and identifies the reporting integration instance
-- `agent` is required and may be any non-empty label string
-- built-in names like `pi` are normalized to their public label form
-- custom labels like `hermes` are accepted as-is
-- while this authority is active, the reported `agent` and `state` override heuristic display for that pane
-- process detection still owns pane liveness and fallback when hook authority is cleared or released
-- `message` is optional metadata for the reporting integration
-- `custom_status` is an optional short display label such as `scheduled`, `indexing`, or `stuck`
-- `custom_status` is visual-only; use `state` for semantic behavior like working or blocked
-- omitting `custom_status` clears any previous custom status from this reporting source
-
-returns `ok`.
-
-### `pane.clear_agent_authority`
-
-params:
-
-```json
-{
- "pane_id": "1-1",
- "source": "custom:hermes"
-}
-```
-
-notes:
-
-- `source` is optional
-- when `source` is omitted, any hook authority for that pane is cleared
-- when `source` is present, only that reporting source is cleared
-
-returns `ok`.
-
-### `pane.release_agent`
-
-use this when the reported agent is exiting cleanly and wants herdr to drop agent identity immediately instead of waiting for fallback detection.
-
-params:
-
-```json
-{
- "pane_id": "1-1",
- "source": "custom:hermes",
- "agent": "hermes"
-}
-```
-
-notes:
-
-- `agent` uses the same non-empty label rules as `pane.report_agent`
-- this clears the pane's effective agent identity immediately when the source and label match the active authority
-- for built-in detected agents, herdr also applies its normal short reacquire suppression during graceful release
-
-returns `ok`.
-
-### `pane.close`
-
-params:
-
-```json
-{
- "pane_id": "1-2"
-}
-```
-
-returns `ok`.
-
-## waits
-
-### `pane.wait_for_output`
-
-this is the direct socket-side one-shot blocking wait.
-
-params:
-
-```json
-{
- "pane_id": "1-1",
- "source": "recent",
- "lines": 200,
- "match": { "type": "substring", "value": "ready" },
- "timeout_ms": 30000,
- "strip_ansi": true
-}
-```
-
-matcher forms:
-
-```json
-{ "type": "substring", "value": "ready" }
-```
-
-```json
-{ "type": "regex", "value": "server.*ready" }
-```
-
-notes:
-
-- `source` must be `visible`, `recent`, or `recent_unwrapped`
-- `lines` is optional
-- `timeout_ms` is optional
-- `strip_ansi` defaults to `true`
-- for `source = "recent"`, output matching uses unwrapped recent terminal text so soft wraps do not break matches
-- `source = "recent_unwrapped"` is also available on `pane.read` when you want to inspect the same unwrapped transcript directly
-- on success you get `output_matched`
-- on timeout you get an error response with code `timeout`
-
-example success response:
-
-```json
-{
- "id": "req_wait",
- "result": {
- "type": "output_matched",
- "pane_id": "1-1",
- "revision": 0,
- "matched_line": "server ready",
- "read": {
- "pane_id": "1-1",
- "workspace_id": "1",
- "tab_id": "1:1",
- "source": "recent_unwrapped",
- "text": "...server ready...",
- "revision": 0,
- "truncated": false
- }
- }
-}
-```
-
-## subscriptions
-
-`events.subscribe` is the long-lived pubsub entrypoint.
-
-you send a subscribe request once, get an ack on the same connection, and then keep reading newline-delimited json events from that same socket.
-
-### subscription ack
-
-```json
-{
- "id": "sub_1",
- "result": {
- "type": "subscription_started"
- }
-}
-```
-
-### supported subscriptions
-
-base lifecycle subscriptions:
-
-- `workspace.created`
-- `workspace.closed`
-- `workspace.focused`
-- `tab.created`
-- `tab.closed`
-- `tab.focused`
-- `tab.renamed`
-- `pane.created`
-- `pane.closed`
-- `pane.focused`
-- `pane.exited`
-- `pane.agent_detected`
-
-parameterized subscriptions:
-
-- `pane.output_matched`
-- `pane.agent_status_changed`
-
-### event naming rule
-
-this part matters because the pushed event names are **not all shaped the same**.
-
-- when you subscribe to a **base lifecycle event**, the pushed `event` value uses snake_case with underscores:
- - subscribe with `workspace.created`
- - receive `workspace_created`
-- when you subscribe to a **parameterized subscription**, the pushed `event` value keeps the dotted name:
- - subscribe with `pane.output_matched`
- - receive `pane.output_matched`
-
-examples below show both forms.
-
-### example: subscribe to lifecycle events
-
-request:
-
-```json
-{
- "id": "sub_life",
- "method": "events.subscribe",
- "params": {
- "subscriptions": [
- { "type": "workspace.created" },
- { "type": "workspace.focused" },
- { "type": "tab.created" },
- { "type": "tab.focused" },
- { "type": "tab.renamed" },
- { "type": "tab.closed" },
- { "type": "pane.created" },
- { "type": "pane.focused" },
- { "type": "pane.agent_detected" },
- { "type": "pane.closed" },
- { "type": "workspace.closed" }
- ]
- }
-}
-```
-
-example pushed event:
-
-```json
-{
- "event": "workspace_created",
- "data": {
- "workspace": {
- "workspace_id": "1",
- "number": 1,
- "label": "herdr",
- "focused": true,
- "pane_count": 1,
- "tab_count": 1,
- "active_tab_id": "1:1",
- "agent_status": "unknown"
- }
- }
-}
-```
-
-### example: subscribe to output matches and agent status changes
-
-request:
-
-```json
-{
- "id": "sub_1",
- "method": "events.subscribe",
- "params": {
- "subscriptions": [
- {
- "type": "pane.output_matched",
- "pane_id": "1-1",
- "source": "recent",
- "lines": 200,
- "match": { "type": "substring", "value": "ready" }
- },
- {
- "type": "pane.agent_status_changed",
- "pane_id": "1-1",
- "agent_status": "done"
- }
- ]
- }
-}
-```
-
-notes:
-
-- `pane.output_matched` supports `source`, optional `lines`, matcher config, and optional `strip_ansi`
-- `pane.agent_status_changed` accepts an optional `agent_status` filter; if omitted, any status transition for that pane can match
-
-example pushed `pane.output_matched` event:
-
-```json
-{
- "event": "pane.output_matched",
- "data": {
- "pane_id": "1-1",
- "matched_line": "server ready",
- "read": {
- "pane_id": "1-1",
- "workspace_id": "1",
- "tab_id": "1:1",
- "source": "recent_unwrapped",
- "text": "...server ready...",
- "revision": 0,
- "truncated": false
- }
- }
-}
-```
-
-example pushed `pane.agent_status_changed` event:
-
-```json
-{
- "event": "pane.agent_status_changed",
- "data": {
- "pane_id": "1-1",
- "workspace_id": "1",
- "agent_status": "done",
- "agent": "pi",
- "custom_status": "scheduled"
- }
-}
-```
-
-`agent` in pushed events follows the same rules as `pane_info.agent`: it may be a built-in detected name, a custom hook-reported label, or omitted. `custom_status` may be included when the pane has hook-owned custom status metadata at the time of the semantic status transition.
-## cli wrappers
-
-these commands provide the shell-facing control surface. most command groups talk to the local socket; `status client` only inspects the local executable.
-
-### command groups
-
-status commands:
-
-```text
-herdr status
-herdr status server
-herdr status client
-```
-
-`herdr -V` and `herdr --version` print the local executable version without contacting the server. `herdr status` compares that local executable with the running server when one is reachable.
-
-workspace commands:
-
-```text
-herdr workspace list
-herdr workspace create [--cwd PATH] [--label TEXT] [--focus] [--no-focus]
-herdr workspace get <workspace_id>
-herdr workspace focus <workspace_id>
-herdr workspace rename <workspace_id> <label>
-herdr workspace close <workspace_id>
-```
-
-tab commands:
-
-```text
-herdr tab list [--workspace <workspace_id>]
-herdr tab create [--workspace <workspace_id>] [--cwd PATH] [--label TEXT] [--focus] [--no-focus]
-herdr tab get <tab_id>
-herdr tab focus <tab_id>
-herdr tab rename <tab_id> <label>
-herdr tab close <tab_id>
-```
-
-agent commands:
-
-```text
-herdr agent list
-herdr agent get <target>
-herdr agent read <target> [--source visible|recent|recent-unwrapped] [--lines N] [--format text|ansi] [--ansi]
-herdr agent send <target> <text>
-herdr agent rename <target> <name>|--clear
-herdr agent focus <target>
-herdr agent wait <target> --status <idle|working|blocked|unknown> [--timeout MS]
-herdr agent attach <target> [--takeover]
-herdr agent start <name> [--cwd PATH] [--workspace ID] [--tab ID] [--split right|down] [--focus|--no-focus] -- <argv...>
-```
-
-agent targets accept terminal ids, unique agent names, detected/reported agent labels, and legacy pane ids. `agent list` shows terminals with explicit agent identity, detected/reported agent identity, or `agent.start` launch metadata. `pane.rename` does not make a plain terminal appear in `agent list`; use `agent.rename` for agent identity.
-
-terminal commands:
-
-```text
-herdr terminal attach <terminal_id> [--takeover]
-```
-
-`terminal attach` is the low-level direct attach primitive. it streams the current rendered terminal state first, then live ANSI frames. direct attach has one writable input/resize owner; a second direct attach fails unless you pass `--takeover`. detach with `ctrl+b q`; send a literal `ctrl+b` with `ctrl+b ctrl+b`.
-
-pane commands:
-
-```text
-herdr pane list [--workspace <workspace_id>]
-herdr pane get <pane_id>
-herdr pane rename <pane_id> <label>|--clear
-herdr pane read <pane_id> [--source visible|recent|recent-unwrapped] [--lines N] [--format text|ansi] [--ansi]
-herdr pane split <pane_id> --direction right|down [--cwd PATH] [--focus] [--no-focus]
-herdr pane close <pane_id>
-herdr pane send-text <pane_id> <text>
-herdr pane send-keys <pane_id> <key> [key ...]
-herdr pane report-agent <pane_id> --source ID --agent LABEL --state idle|working|blocked|unknown [--message TEXT] [--custom-status TEXT] [--seq N]
-herdr pane run <pane_id> <command>
-```
-
-wait commands:
-
-```text
-herdr wait output <pane_id> --match <text> [--source visible|recent|recent-unwrapped] [--lines N] [--timeout MS] [--regex] [--raw]
-herdr wait agent-status <pane_id> --status <idle|working|blocked|done|unknown> [--timeout MS]
-```
-
-### cli behavior notes
-
-- `status` prints local client version/protocol, running server version/protocol when reachable, socket path, compatibility, and whether a restart is needed
-- `status server` prints only the running server side; if no server is reachable it exits successfully and prints `status: not running`
-- `status client` prints only the local executable version/protocol and binary path without contacting the server
-- `workspace create` keeps focus where it is by default; pass `--focus` to switch to the new workspace
-- `workspace create` without `--label` keeps the default cwd-based workspace naming
-- `workspace create --label` applies the custom workspace name immediately
-- `workspace create` returns `result.workspace`, `result.tab`, and `result.root_pane`
-- `tab create` keeps focus where it is by default; pass `--focus` to switch to the new tab
-- `tab create` without `--label` keeps the default numbered tab naming
-- `tab create --label` applies the custom tab name immediately
-- `tab create` returns `result.tab` and `result.root_pane`
-- `agent list` shows agent terminals, not arbitrary named panes
-- `agent wait --status idle` is the CLI completion wait; it treats the UI-only `done` attention state as satisfying idle
-- `agent focus` switches the TUI workspace/tab/pane focus; it is not direct terminal attach
-- `agent attach` attaches the current terminal directly to the agent terminal stream
-- `agent start <name> -- <argv...>` starts a named agent terminal and returns `result.agent` plus `result.argv`
-- `pane split` keeps focus where it is by default; pass `--focus` to switch to the new pane
-- `pane read` prints **text**, not json
-- `pane read --format ansi` and `pane read --ansi` print a rendered ANSI snapshot with colors/styles preserved
-- `pane read --source recent-unwrapped` returns recent terminal text with soft wraps joined back together
-- `pane send-text`, `pane send-keys`, `pane report-agent`, and `pane run` print nothing on success
-- list/get/create/split/wait commands print json on success
-- `pane run` is a convenience wrapper for `pane.send_input` with the command text followed by a real `Enter` keypress
-- `wait agent-status` is a cli convenience built on top of event subscriptions
-- use it when you want the same `done` / `idle` distinction the UI shows
-- `--raw` is a legacy alias for ANSI formatted `pane read` output and still disables ansi stripping for `wait output`
-- `wait output --source recent` matches against unwrapped recent terminal text by default, so pane width and soft wrapping do not break matches
-
-### cli examples
-
-create a workspace, split a pane, run a server, and wait for readiness:
-
-```bash
-herdr workspace create --cwd /path/to/project --label "api server"
-herdr pane split 1-1 --direction right --no-focus
-herdr pane run 1-2 "npm run dev"
-herdr wait output 1-2 --match "ready" --timeout 30000
-```
-
-wait for an agent by name to become idle for CLI automation:
-
-```bash
-herdr agent wait reviewer --status idle --timeout 60000
-```
-
-wait for a pane-level UI attention state:
-
-```bash
-herdr wait agent-status 1-1 --status done --timeout 60000
-```
-
-inspect another pane's output:
-
-```bash
-herdr pane read 1-1 --source recent --lines 80
-```
-
-## behavior notes and gotchas
-
-- `pane.send_text` sends literal text only. if you want to execute a command, follow it with `pane.send_keys` and `Enter`, use `pane.send_input` for ordered `text + keypress` input, or use cli `pane run`, which sends the text and then a real Enter key in one request.
-- `pane.read` and `pane.wait_for_output` strip ansi by default.
-- `pane.output_matched` subscriptions fire on transitions into a matching state; they do not repeatedly spam the same still-visible match on every poll.
-- closing the socket connection ends the subscription.
-- there is no separate event transport.
-- the same herdr process can serve regular request/response calls and long-lived subscription connections at the same time.
diff --git a/docs/next/CHANGELOG.md b/docs/next/CHANGELOG.md
index 7e60e568..02f23fad 100644
--- a/docs/next/CHANGELOG.md
+++ b/docs/next/CHANGELOG.md
@@ -151,7 +151,7 @@
## [0.5.1] - 2026-04-25
### Added
-- Toast notifications can now be delivered through the outer terminal as desktop notifications. Configure this with `ui.toast.delivery = "terminal"`; see `CONFIGURATION.md` for details.
+- Toast notifications can now be delivered through the outer terminal as desktop notifications. Configure this with `ui.toast.delivery = "terminal"`; see the [configuration docs](https://herdr.dev/docs/configuration/) for details.
- Herdr now writes separate capped support logs for app, client, and server modes, making persistent-session issue reports easier to diagnose without unbounded log growth.
- The bundled opencode plugin now reports question prompts as blocked while waiting for user input, then returns to working or idle when answered or dismissed. Question prompts are also detected by the default terminal-screen heuristics. (#51, thanks @mspiegel31)
@@ -428,7 +428,7 @@
### Added
- Added first-run onboarding flow that lets you choose notification preferences (sound and toast) on startup.
- Added optional visual toast notifications in the top-right corner for background workspace events (completion and attention-needed alerts).
-- Added configurable keybindings for all navigate mode actions: new workspace, rename workspace, close workspace, resize mode, and toggle sidebar. See `CONFIGURATION.md` for the full key reference.
+- Added configurable keybindings for all navigate mode actions: new workspace, rename workspace, close workspace, resize mode, and toggle sidebar. See the [configuration docs](https://herdr.dev/docs/configuration/) for the full key reference.
- Added configuration validation with startup diagnostics. Invalid key combinations or duplicate bindings now fall back to safe defaults with a visible warning.
### Changed
@@ -439,7 +439,7 @@
- Keybinding parser now accepts special keys (`enter`, `esc`, `tab`, `backspace`, `space`) and function keys (`f1`–`f12`).
### Documentation
-- Split configuration reference into a dedicated `CONFIGURATION.md` file with full keybinding documentation and config diagnostics explanation.
+- Split configuration reference into dedicated configuration docs with full keybinding documentation and config diagnostics explanation.
## [0.1.1] - 2026-03-28
diff --git a/docs/next/CONFIGURATION.md b/docs/next/CONFIGURATION.md
deleted file mode 100644
index 9d85b53b..00000000
--- a/docs/next/CONFIGURATION.md
+++ /dev/null
@@ -1,493 +0,0 @@
-# configuration
-
-herdr reads config from:
-
-```text
-~/.config/herdr/config.toml
-```
-
-Named sessions share this config file. Sessions are runtime/socket namespaces, not workspace replacements; per-session sockets and persistent runtime state are separate:
-
-```text
-~/.config/herdr/session.json
-~/.config/herdr/sessions/<name>/session.json
-```
-
-Use `herdr session list`, `herdr session attach <name>`, `herdr session stop <name>`, and `herdr session delete <name>` to inspect and manage named session namespaces. Add `--json` to session commands when scripts need machine-readable output.
-
-In default persistence mode, quitting the UI detaches the current client. Use `herdr server stop` to stop the shared background server.
-
-print the full default config with:
-
-```bash
-herdr --default-config
-```
-
-if a config value is invalid, or two navigate actions use the same keybinding, herdr falls back to a safe default and shows a startup warning in the UI.
-
-## live reload
-
-After editing `config.toml`, reload the running app without restarting the persistent server:
-
-```bash
-herdr server reload-config
-```
-
-You can also use the global menu inside herdr and choose `reload config`.
-
-Reload is server-owned. In persistent mode the CLI sends a request to the running server, and the server reads, parses, validates, and applies `config.toml`.
-
-Reloadable now:
-- keybindings and prefix
-- theme, custom theme colors, and legacy `ui.accent`
-- `ui.confirm_close`
-- `ui.agent_panel_scope`
-- `ui.toast.delivery`
-- server-side `ui.sound` policy; attached thin clients refresh local sound config after a successful sound-policy change
-- `experimental.kitty_graphics`
-- `advanced.scrollback_limit_bytes` for panes created after reload
-- `ui.sidebar_width` as the default width; current width updates only while it is still config-owned
-- `ui.sidebar_min_width` and `ui.sidebar_max_width`; the live width is re-clamped to the new bounds on reload
-
-Startup-only or special-case:
-- `onboarding` does not reopen onboarding during reload
-- `experimental.allow_nested` is checked before launch and needs a restart
-- existing pane scrollback buffers are not resized during reload
-- terminal notifications and sounds are client-local side effects and are sent to the foreground attached client
-
-If the TOML cannot be read or parsed, reload applies nothing and keeps the current running state. If keybindings are invalid, herdr keeps the current keybindings while applying other valid reloadable settings where possible.
-
-## onboarding
-
-```toml
-onboarding = true
-```
-
-| option | default | description |
-|--------|---------|-------------|
-| `onboarding` | unset | show first-run notification setup; set `false` after choosing |
-
-notes:
-- missing `onboarding` currently behaves like `true`
-- set `onboarding = true` to force the setup screen again for testing
-- continuing from onboarding writes `onboarding = false` and opens the normal settings UI
-
-## keybindings
-
-keybindings live under `[keys]`.
-
-supported syntax:
-- plain keys: `n`, `x`, `-`, `` ` ``
-- modifiers: `ctrl+b`, `shift+n`, `alt+x`, `cmd+x`, `super+x`
-- special keys: `enter`, `esc`, `tab`, `backspace`, `left`, `right`, `up`, `down`
-- function keys: `f1`, `f12`
-- uppercase letters also imply shift: `D` works like `shift+d`
-
-notes:
-- most reliable bindings are plain keys, `ctrl+letter`, `esc`/`tab`/`enter`, and function keys
-- `alt+...`, `cmd`/`super`, and punctuation-with-modifiers may vary depending on terminal/tmux setup
-- bindings marked `unset` in the key reference are supported actions with no default key assigned
-- for navigate-mode actions, duplicate keybindings are treated as config errors; later conflicting bindings fall back to defaults
-
-example:
-
-```toml
-[keys]
-prefix = "ctrl+b"
-new_workspace = "n"
-rename_workspace = "shift+n"
-close_workspace = "X"
-reload_config = "" # optional, unset by default
-open_notification_target = "" # optional, unset by default
-new_tab = "c"
-split_vertical = "d"
-split_horizontal = "D"
-close_pane = "x"
-rename_pane = "" # optional, unset by default
-edit_scrollback = "" # optional, opens focused pane scrollback in $EDITOR
-zoom = "f" # legacy alias: fullscreen
-resize_mode = "r"
-toggle_sidebar = "b"
-previous_workspace = "ctrl+alt+["
-next_workspace = "ctrl+alt+]"
-previous_agent = "ctrl+["
-next_agent = "ctrl+]"
-previous_tab = "alt+["
-next_tab = "alt+]"
-focus_pane_left = "alt+h"
-focus_pane_down = "alt+j"
-focus_pane_up = "alt+k"
-focus_pane_right = "alt+l"
-
-[keys.indexed]
-tabs = "" # optional; e.g. "ctrl" makes ctrl+1..9 switch tabs
-workspaces = "" # optional; e.g. "ctrl+shift" makes ctrl+shift+1..9 switch workspaces
-agents = "" # optional; follows visible agent panel order
-```
-
-### key reference
-
-| key | default | action |
-|-----|---------|--------|
-| `prefix` | `ctrl+b` | enter or leave navigate mode |
-| `new_workspace` | `n` | create a new workspace |
-| `rename_workspace` | `shift+n` | rename selected workspace |
-| `close_workspace` | `shift+d` | close selected workspace |
-| `detach` | unset | optional explicit detach shortcut in the persistent session |
-| `reload_config` | unset | reload `config.toml` in the running app/server |
-| `open_notification_target` | unset | jump to the currently visible notification target |
-| `previous_workspace` | unset | switch to the previous workspace directly from terminal mode |
-| `next_workspace` | unset | switch to the next workspace directly from terminal mode |
-| `previous_agent` | unset | focus the previous agent shown in the sidebar agent list |
-| `next_agent` | unset | focus the next agent shown in the sidebar agent list |
-| `new_tab` | `c` | create a new tab |
-| `rename_tab` | unset | rename the active tab |
-| `previous_tab` | unset | switch to the previous tab directly from terminal mode |
-| `next_tab` | unset | switch to the next tab directly from terminal mode |
-| `close_tab` | unset | close the active tab |
-| `focus_pane_left` | unset | focus the pane to the left directly from terminal mode |
-| `focus_pane_down` | unset | focus the pane below directly from terminal mode |
-| `focus_pane_up` | unset | focus the pane above directly from terminal mode |
-| `focus_pane_right` | unset | focus the pane to the right directly from terminal mode |
-| `split_vertical` | `v` | split pane vertically (side by side) |
-| `split_horizontal` | `-` | split pane horizontally (stacked) |
-| `close_pane` | `x` | close focused pane |
-| `rename_pane` | unset | rename the focused pane |
-| `edit_scrollback` | unset | open the focused pane's retained scrollback in `$EDITOR` inside a temporary zoomed pane |
-| `zoom` | `f` | zoom focused pane; legacy alias: `fullscreen` |
-| `resize_mode` | `r` | enter or leave resize mode |
-| `toggle_sidebar` | `b` | collapse or expand the sidebar |
-
-`edit_scrollback` writes the focused pane's retained plain-text scrollback to a temporary file, opens `${EDITOR:-vi}` on that file in a temporary zoomed pane, then removes the file when the editor exits.
-
-### indexed keybindings
-
-Use `[keys.indexed]` to bind number keys `1` through `9` as positional shortcuts. Each value is a modifier combo only. Empty values disable that shortcut family.
-
-```toml
-[keys.indexed]
-tabs = ""
-workspaces = ""
-agents = ""
-```
-
-| key | default | action |
-|-----|---------|--------|
-| `tabs` | unset | switch to tab 1-9 in the active workspace, left to right |
-| `workspaces` | unset | switch to workspace 1-9 in sidebar order, top to bottom |
-| `agents` | unset | focus agent row 1-9 in the visible agent panel order |
-
-### custom command keybindings
-
-Use `[[keys.command]]` to bind a prefix-mode key to a command. Press the prefix key, then the configured key.
-
-```toml
-[[keys.command]]
-key = "g"
-type = "pane"
-command = "lazygit"
-```
-
-`type` is optional and defaults to `shell`.
-
-| type | behavior |
-|------|----------|
-| `shell` | run the command detached in the background |
-| `pane` | open a temporary zoomed pane, run the command there, then close the pane when the command exits |
-
-Commands run through `/bin/sh -lc`. Herdr sets the command working directory to the active pane cwd when available and provides context through environment variables:
-
-| variable | value |
-|----------|-------|
-| `HERDR_SOCKET_PATH` | active herdr socket path |
-| `HERDR_BIN_PATH` | current herdr binary path |
-| `HERDR_ACTIVE_WORKSPACE_ID` | active workspace id |
-| `HERDR_ACTIVE_TAB_ID` | active tab id |
-| `HERDR_ACTIVE_PANE_ID` | focused pane id |
-| `HERDR_ACTIVE_PANE_CWD` | focused pane cwd |
-
-Example detached helper:
-
-```toml
-[[keys.command]]
-key = "shift+g"
-type = "shell"
-command = "notify-send herdr 'custom command ran'"
-```
-
-## theme
-
-herdr ships with 18 built-in color themes. set one in config:
-
-```toml
-[theme]
-name = "tokyo-night"
-```
-
-### built-in themes
-
-| name | description |
-|------|-------------|
-| `catppuccin` | soft pastel mocha palette (default) |
-| `catppuccin-latte` | light catppuccin palette |
-| `terminal` | use your terminal's 16-color ANSI palette |
-| `tokyo-night` | blue-purple aesthetic |
-| `tokyo-night-day` | light tokyo night palette |
-| `dracula` | purple/pink/green classic |
-| `nord` | frosty scandinavian blues |
-| `gruvbox` | warm retro browns/oranges |
-| `gruvbox-light` | light gruvbox palette |
-| `one-dark` | atom's beloved dark palette |
-| `one-light` | atom's light palette |
-| `solarized` | ethan schoonover's classic dark palette |
-| `solarized-light` | ethan schoonover's classic light palette |
-| `kanagawa` | hokusai-inspired |
-| `kanagawa-lotus` | light kanagawa palette |
-| `rose-pine` | muted, elegant |
-| `rose-pine-dawn` | light rosé pine palette |
-| `vesper` | high-contrast monochrome with peach and mint accents |
-
-theme names are flexible: `tokyo-night`, `tokyonight`, and `tokyo_night` all work.
-
-### custom overrides
-
-override individual color tokens on top of any base theme:
-
-```toml
-[theme]
-name = "dracula"
-
-[theme.custom]
-panel_bg = "reset"
-accent = "#f5c2e7"
-red = "rgb(255, 85, 85)"
-green = "#a6e3a1"
-```
-
-all tokens are optional — only set what you want to change.
-
-### available tokens
-
-| token | used for |
-|-------|----------|
-| `accent` | highlights, active borders, navigation UI |
-| `panel_bg` | floating panel, tab bar, and overlay background |
-| `surface0` | selected item background |
-| `surface1` | hover/active backgrounds |
-| `surface_dim` | active workspace background, separators |
-| `overlay0` | muted text, secondary info |
-| `overlay1` | slightly brighter secondary text |
-| `text` | primary text |
-| `subtext0` | workspace names, dimmed labels |
-| `mauve` | git branch names, special labels |
-| `green` | idle/done states |
-| `yellow` | busy/running states |
-| `red` | waiting/needs attention states |
-| `blue` | unseen notifications |
-| `teal` | done notification accents |
-| `peach` | interrupted/warning states |
-
-tokens accept the same color formats as `accent`: hex (`#rrggbb`), named colors, or `rgb(r,g,b)`.
-
-for `panel_bg`, you can also use `reset`, `default`, `none`, or `transparent` to stop herdr from painting an opaque panel background and instead use the host terminal's default background.
-
-## ui
-
-```toml
-[ui]
-sidebar_width = 26
-sidebar_min_width = 18
-sidebar_max_width = 36
-mouse_capture = true
-confirm_close = true
-prompt_new_tab_name = true
-show_agent_labels_on_pane_borders = false
-agent_panel_scope = "all"
-accent = "cyan"
-```
-
-### options
-
-| option | default | description |
-|--------|---------|-------------|
-| `sidebar_width` | `26` | base sidebar width before auto-scaling |
-| `sidebar_min_width` | `18` | minimum sidebar width when expanded |
-| `sidebar_max_width` | `36` | maximum sidebar width when expanded |
-| `mouse_capture` | `true` | capture mouse input for Herdr's mouse UI; set false to let the terminal handle normal clicks while still forwarding mouse to pane apps that request it |
-| `confirm_close` | `true` | ask before closing a workspace |
-| `prompt_new_tab_name` | `true` | ask for a tab name before creating a new tab; set false to create tabs immediately with generated names |
-| `show_agent_labels_on_pane_borders` | `false` | show detected/reported agent labels in split pane borders when no manual pane name is set |
-| `agent_panel_scope` | `all` | sidebar agent list scope: `current` or `all` |
-| `accent` | `cyan` | highlight and border color |
-
-Changing the agent panel scope from the sidebar writes `agent_panel_scope` to config so it survives session resets and upgrades.
-
-`accent` accepts:
-- named colors like `cyan`, `blue`, `magenta`
-- hex like `#89b4fa`
-- rgb like `rgb(137,180,250)`
-
-## toast notifications
-
-```toml
-[ui.toast]
-delivery = "off"
-```
-
-### options
-
-| option | default | description |
-|--------|---------|-------------|
-| `ui.toast.delivery` | `off` | where background popup notifications should appear |
-
-available values:
-- `off` — disable popup notifications
-- `herdr` — show top-right in-app toasts
-- `terminal` — ask the outer terminal to show a desktop notification. some terminals suppress foreground notifications, including ghostty on macos.
-- `system` — ask the os notification service directly. on macos, herdr uses `terminal-notifier` when available and falls back to built-in `osascript`. on linux, `system` requires `notify-send`.
-
-### macos system notifications
-
-for best macos support, install `terminal-notifier`:
-
-```sh
-brew install terminal-notifier
-```
-
-when `terminal-notifier` is installed, herdr tries to focus the hosting terminal when a notification is clicked. click-to-return is supported for detected ghostty, iterm2, wezterm, kitty, alacritty, and terminal.app sessions.
-
-without `terminal-notifier`, herdr falls back to built-in `osascript`. this still shows a macos notification, but clicking the notification may focus the apple script runner instead of returning to your terminal.
-
-compatibility note:
-- older configs may still use `ui.toast.enabled = true|false`
-- herdr still reads that legacy key for compatibility
-- if you save toast settings from inside herdr, it rewrites the setting to `ui.toast.delivery`
-
-current behavior:
-- informational only
-- one notification event at a time
-- shown for background agent events like `needs attention` and `finished`
-- suppression is tab-aware: the active tab stays quiet, but background tabs in the same workspace can still notify
-- `terminal` delivery is best-effort and depends on terminal support
-- `system` delivery is best-effort and depends on the os helper being available
-- macos `system` delivery prefers `terminal-notifier` when present and falls back to `osascript`
-- currently targets terminals such as ghostty, kitty, iterm2, and wezterm
-- inside tmux, herdr wraps notification escapes with tmux passthrough
-
-## sound
-
-```toml
-[ui.sound]
-enabled = true
-
-[ui.sound.agents]
-claude = "default"
-droid = "off"
-```
-
-### options
-
-| option | default | description |
-|--------|---------|-------------|
-| `ui.sound.enabled` | `true` | enable background agent sounds |
-
-per-agent values:
-- `default`
-- `on`
-- `off`
-
-available agent keys:
-- `pi`
-- `claude`
-- `codex`
-- `gemini`
-- `cursor`
-- `cline`
-- `open_code`
-- `github_copilot`
-- `kimi`
-- `droid`
-- `amp`
-- `grok`
-- `hermes`
-
-## experimental
-
-```toml
-[experimental]
-allow_nested = false
-kitty_graphics = false
-```
-
-### options
-
-| option | default | description |
-|--------|---------|-------------|
-| `experimental.allow_nested` | `false` | allow launching herdr from inside a herdr-managed pane |
-| `experimental.kitty_graphics` | `false` | enable experimental local Kitty graphics rendering for attached clients |
-
-### nested launches
-
-By default, herdr blocks nested launches when `HERDR_ENV=1` is already present.
-
-Set `allow_nested = true` only for debugging or intentionally nested setups.
-
-### Kitty graphics
-
-`kitty_graphics` enables experimental local Kitty graphics rendering for attached clients.
-
-It requires a Kitty graphics-compatible outer terminal.
-
-Known limitation: resizing the terminal window or changing the terminal font while images are visible can leave existing images misplaced or stale. Restart the pane app or clear and redraw the image output after changing size or font. Please report any findings so this experimental path can improve.
-
-## advanced
-
-```toml
-[advanced]
-scrollback_limit_bytes = 10000000
-```
-
-### options
-
-| option | default | description |
-|--------|---------|-------------|
-| `advanced.scrollback_limit_bytes` | `10000000` | maximum scrollback buffer size in bytes retained per pane terminal |
-
-### scrollback
-
-`scrollback_limit_bytes` limits retained terminal scrollback per pane.
-
-The default matches Ghostty's `scrollback-limit` value.
-
-Set `scrollback_limit_bytes = 0` to disable pane scrollback entirely.
-
-The legacy `scrollback_lines` key is still accepted inside `[advanced]`, but it uses the same byte-based value.
-
-## environment variables
-
-| variable | description |
-|----------|-------------|
-| `HERDR_LOG` | log level filter (default: `herdr=info`) |
-
-## logs
-
-herdr writes local file logs under:
-
-```text
-~/.config/herdr/
-```
-
-common files:
-
-```text
-~/.config/herdr/herdr.log
-~/.config/herdr/herdr-client.log
-~/.config/herdr/herdr-server.log
-```
-
-notes:
-- `herdr.log` is used by monolithic `--no-session` mode and some top-level startup paths
-- persistent session mode mainly uses `herdr-client.log` and `herdr-server.log`
-- logs rotate automatically by size and keep a few older files as `.1`, `.2`, and so on
-- default logs are metadata-focused and are intended to be shareable for issue diagnosis
-- `HERDR_LOG` can increase verbosity when you need a local repro or deeper debugging
diff --git a/docs/next/INTEGRATIONS.md b/docs/next/INTEGRATIONS.md
deleted file mode 100644
index fc47bbc0..00000000
--- a/docs/next/INTEGRATIONS.md
+++ /dev/null
@@ -1,284 +0,0 @@
-# integrations
-
-herdr works without any hook or plugin setup.
-
-out of the box, it detects supported agents automatically by combining foreground process detection with screen heuristics. that is enough to give you workspace awareness with zero configuration.
-
-when an agent exposes hooks or plugins, the more robust path is to forward semantic state to herdr over the local socket api. the built-in integrations in this document do exactly that.
-
-if you want to inspect the exact files herdr installs, they are versioned in this repo:
-
-- [pi extension](./src/integration/assets/pi/herdr-agent-state.ts)
-- [claude code hook](./src/integration/assets/claude/herdr-agent-state.sh)
-- [codex hook](./src/integration/assets/codex/herdr-agent-state.sh)
-- [opencode plugin](./src/integration/assets/opencode/herdr-agent-state.js)
-- [hermes plugin](./src/integration/assets/hermes/__init__.py)
-
-## how herdr uses integrations
-
-herdr uses a hybrid model:
-
-- **process detection** owns pane identity, liveness, and "the process is gone"
-- **agent integrations** report semantic state like `working`, `blocked`, and `idle` over the local socket api when the tool exposes those events
-- **screen heuristics** remain the fallback for gaps, unsupported tools, or incomplete hook surfaces
-
-that means hooks/plugins do **not** become the source of truth for pane ownership. they enrich state reporting; they do not replace process detection.
-
-## install commands
-
-```bash
-herdr integration install pi
-herdr integration install claude
-herdr integration install codex
-herdr integration install opencode
-herdr integration install hermes
-```
-
-## uninstall commands
-
-```bash
-herdr integration uninstall pi
-herdr integration uninstall claude
-herdr integration uninstall codex
-herdr integration uninstall opencode
-herdr integration uninstall hermes
-```
-
-## pi
-
-install:
-
-```bash
-herdr integration install pi
-```
-
-this writes the bundled pi extension to:
-
-```text
-~/.pi/agent/extensions/herdr-agent-state.ts
-```
-
-if `PI_CODING_AGENT_DIR` is set, herdr uses that agent directory instead and writes to:
-
-```text
-$PI_CODING_AGENT_DIR/extensions/herdr-agent-state.ts
-```
-
-`~` is expanded in `PI_CODING_AGENT_DIR`.
-
-pi is the cleanest integration. it already has an authoritative hook model, so herdr can get direct state reports over the socket api without guessing as much from the terminal.
-
-bundled source: [`src/integration/assets/pi/herdr-agent-state.ts`](./src/integration/assets/pi/herdr-agent-state.ts)
-
-uninstall:
-
-```bash
-herdr integration uninstall pi
-```
-
-this removes the same extension path herdr would install, using `PI_CODING_AGENT_DIR` when it is set.
-
-## claude code
-
-install:
-
-```bash
-herdr integration install claude
-```
-
-this:
-
-- writes the hook script to `~/.claude/hooks/herdr-agent-state.sh`
-- updates `~/.claude/settings.json`
-
-if `CLAUDE_CONFIG_DIR` is set, herdr uses that directory instead, for example `$CLAUDE_CONFIG_DIR/hooks/herdr-agent-state.sh` and `$CLAUDE_CONFIG_DIR/settings.json`. `~` is expanded in `CLAUDE_CONFIG_DIR`.
-
-bundled source: [`src/integration/assets/claude/herdr-agent-state.sh`](./src/integration/assets/claude/herdr-agent-state.sh)
-
-current hook mapping:
-
-- `UserPromptSubmit` → `working`
-- `PreToolUse` → `working`
-- `PermissionRequest` → `blocked`
-- `PostToolUse` → `working`
-- `PostToolUseFailure` → `working`
-- `SubagentStop` → `working`
-- `Stop` → `idle`
-- `SessionEnd` → `release`
-
-notes:
-
-- claude code hooks also run inside subagents. herdr treats subagent `working` and `blocked` reports as real pane state.
-- subagent stop/release events are converted to `working` by the bundled hook script so a completed subagent does not make the parent claude pane look idle.
-- `PostToolUse` and `PostToolUseFailure` move the pane back to `working` after a permissioned tool call resolves.
-- some non-claude tools, including grok cli, may load claude settings or plugins. herdr ignores conflicting known-agent hook labels once native foreground-process detection identifies a different known agent.
-
-uninstall:
-
-```bash
-herdr integration uninstall claude
-```
-
-this removes the same hook path and settings entries herdr would install, using `CLAUDE_CONFIG_DIR` when it is set.
-
-## codex
-
-install:
-
-```bash
-herdr integration install codex
-```
-
-this:
-
-- writes the hook script to `~/.codex/herdr-agent-state.sh`
-- updates `~/.codex/hooks.json`
-- ensures `hooks = true` under `[features]` in `~/.codex/config.toml`
-- migrates the deprecated top-level `[features] codex_hooks = true` setting to `hooks = true`
-
-if `CODEX_HOME` is set, herdr uses that directory instead, for example `$CODEX_HOME/herdr-agent-state.sh`, `$CODEX_HOME/hooks.json`, and `$CODEX_HOME/config.toml`. `~` is expanded in `CODEX_HOME`.
-
-bundled source: [`src/integration/assets/codex/herdr-agent-state.sh`](./src/integration/assets/codex/herdr-agent-state.sh)
-
-current hook mapping:
-
-- `SessionStart` → `idle`
-- `UserPromptSubmit` → `working`
-- `PreToolUse` → `working`
-- `Stop` → `idle`
-
-notes:
-
-- codex does **not** currently expose a permission-specific hook like claude or opencode, so `blocked` still depends on herdr's normal heuristics.
-- codex currently renders hook lifecycle messages in its own tui, for example `Running SessionStart hook` and `SessionStart hook (completed)`.
-- that noise is an upstream codex limitation, not a herdr-specific issue.
-- codex has a `suppressOutput` field in its hook output schema, but it is currently not effective for suppressing those tui lifecycle lines.
-
-uninstall:
-
-```bash
-herdr integration uninstall codex
-```
-
-this:
-
-- removes the same hook path herdr would install, using `CODEX_HOME` when it is set
-- removes herdr-owned hook entries from the matching `hooks.json`
-- intentionally leaves the matching `config.toml` alone
-
-that last point is deliberate: herdr does **not** try to guess whether `hooks = true` is still needed for some other codex hook setup.
-
-## opencode
-
-install:
-
-```bash
-herdr integration install opencode
-```
-
-this writes the bundled plugin to:
-
-```text
-~/.config/opencode/plugins/herdr-agent-state.js
-```
-
-bundled source: [`src/integration/assets/opencode/herdr-agent-state.js`](./src/integration/assets/opencode/herdr-agent-state.js)
-
-current plugin mapping:
-
-- `permission.asked` → `blocked`
-- `permission.replied: once|always` → `working`
-- `permission.replied: reject` → `idle`
-- `question.asked` → `blocked`
-- `question.replied` → `working`
-- `question.rejected` → `idle`
-- `session.status: busy|retry` → `working`
-- `session.status: idle` → `idle`
-- `session.idle` → `idle`
-
-notes:
-
-- opencode has the richest event surface of the currently supported integrations.
-- herdr intentionally does **not** guess that `session.deleted` means process exit. process detection still owns liveness and pane identity.
-
-uninstall:
-
-```bash
-herdr integration uninstall opencode
-```
-
-this removes:
-
-```text
-~/.config/opencode/plugins/herdr-agent-state.js
-```
-
-## hermes agent
-
-install:
-
-```bash
-herdr integration install hermes
-```
-
-this:
-
-- writes the bundled plugin to `~/.hermes/plugins/herdr-agent-state/`
-- updates `~/.hermes/config.yaml` so `plugins.enabled` contains `herdr-agent-state`
-
-bundled source: [`src/integration/assets/hermes/__init__.py`](./src/integration/assets/hermes/__init__.py)
-
-current plugin mapping:
-
-- `on_session_start` → `idle`
-- `pre_llm_call` → `working`
-- `pre_api_request` → `working`
-- `pre_tool_call` → `working`
-- `post_tool_call` → `working`
-- `pre_approval_request` → `blocked`
-- `post_approval_response` → `working`
-- `post_llm_call` → `idle`
-- `on_session_end` → `idle`
-- `on_session_finalize` → `release`
-
-notes:
-
-- hermes plugins are opt-in, so install also enables the plugin in `config.yaml`.
-- herdr still keeps native process detection and screen heuristics for hermes. the plugin improves state accuracy when the hook surface fires.
-
-uninstall:
-
-```bash
-herdr integration uninstall hermes
-```
-
-this removes `~/.hermes/plugins/herdr-agent-state/` and removes the matching `plugins.enabled` entry from `~/.hermes/config.yaml`.
-
-## grok cli
-
-herdr does not currently install a grok hook or plugin.
-
-grok is heuristic-only in herdr. herdr detects `grok` and `grok-build` from the foreground process and uses screen heuristics for `working`, `blocked`, and `idle` states.
-
-grok cli may load claude settings or plugins as a compatibility feature, including hooks from `~/.claude/settings.json`. if that causes the claude herdr hook to run in a grok pane, herdr ignores the conflicting `claude` hook label once native foreground-process detection identifies the pane as `grok`.
-
-## amp
-
-herdr does not currently install an Amp plugin.
-
-Amp's public plugin API exposes lifecycle and tool-call hooks, but not passive permission/request-blocked events. A Herdr Amp plugin that only reports `idle` and `working` would take hook authority for the pane and mask Herdr's existing screen heuristics for Amp `blocked` states. Until Amp exposes permission state as an observable plugin event, Amp remains heuristic-only in Herdr.
-
-## known limitations
-
-- these integrations only activate inside herdr-managed panes.
-- if an agent has an incomplete hook surface, herdr falls back to process detection and screen heuristics rather than inventing lease or ttl behavior.
-- codex currently shows hook lifecycle chatter in its own tui until upstream adds a real silent mode.
-
-## troubleshooting
-
-if an install command succeeds but you do not see improved state reporting:
-
-1. make sure you launched the agent inside a herdr pane
-2. restart the agent session so it picks up the new hook/plugin config
-3. verify the expected config file was written to the path above
-4. remember that unsupported transitions still fall back to herdr's built-in heuristics
diff --git a/docs/next/README.md b/docs/next/README.md
index ee6b70e3..0a856f63 100644
--- a/docs/next/README.md
+++ b/docs/next/README.md
@@ -6,7 +6,7 @@
</p>
<p align="center">
- <a href="https://herdr.dev">herdr.dev</a> · <a href="#install">install</a> · <a href="#quick-start">quick start</a> · <a href="#supported-agents">supported agents</a> · <a href="./INTEGRATIONS.md">integrations</a> · <a href="./CONFIGURATION.md">configuration</a> · <a href="./SOCKET_API.md">socket api</a>
+ <a href="https://herdr.dev">herdr.dev</a> · <a href="#install">install</a> · <a href="#quick-start">quick start</a> · <a href="#supported-agents">supported agents</a> · <a href="https://herdr.dev/docs/integrations/">integrations</a> · <a href="https://herdr.dev/docs/configuration/">configuration</a> · <a href="https://herdr.dev/docs/socket-api/">socket api</a>
</p>
---
@@ -163,7 +163,7 @@ herdr pane read 1-2 --source recent --lines 50
herdr pane read 1-2 --source visible --ansi
```
-full reference: [`SOCKET_API.md`](./SOCKET_API.md) and [`SKILL.md`](./SKILL.md).
+full reference: [socket api](https://herdr.dev/docs/socket-api/) and [`SKILL.md`](./SKILL.md).
## supported agents
@@ -182,7 +182,7 @@ automatic detection works out of the box. process name matching plus terminal ou
detected but not fully tested: gemini cli, cursor agent, cline, kimi, github copilot cli.
-for agents outside the built-in list, herdr still works as a terminal multiplexer with workspaces, panes, and tiling. custom integrations can report agent labels over the socket api. see [`SOCKET_API.md`](./SOCKET_API.md).
+for agents outside the built-in list, herdr still works as a terminal multiplexer with workspaces, panes, and tiling. custom integrations can report agent labels over the socket api. see the [socket api docs](https://herdr.dev/docs/socket-api/).
### direct integrations
@@ -196,7 +196,7 @@ herdr integration install opencode
herdr integration install hermes
```
-see [`INTEGRATIONS.md`](./INTEGRATIONS.md) for setup details.
+see the [integrations docs](https://herdr.dev/docs/integrations/) for setup details.
## keybindings
@@ -226,7 +226,7 @@ type = "pane" # "shell" or "pane"
command = "lazygit"
```
-mouse is supported throughout. full reference: [`CONFIGURATION.md`](./CONFIGURATION.md).
+mouse is supported throughout. full reference: [configuration docs](https://herdr.dev/docs/configuration/).
## configuration
@@ -236,7 +236,7 @@ config file: `~/.config/herdr/config.toml`
herdr --default-config # print full default config
```
-in-app settings screen for theme, sound, and toast preferences. full reference: [`CONFIGURATION.md`](./CONFIGURATION.md).
+in-app settings screen for theme, sound, and toast preferences. full reference: [configuration docs](https://herdr.dev/docs/configuration/).
## logs
@@ -260,14 +260,14 @@ use a higher log level only when needed:
HERDR_LOG=herdr=debug herdr
```
-full logging and environment variable details: [`CONFIGURATION.md`](./CONFIGURATION.md).
+full logging and environment variable details: [configuration docs](https://herdr.dev/docs/configuration/).
## docs
-- [`CONFIGURATION.md`](./CONFIGURATION.md) — keybindings, themes, notifications, environment variables
-- [`INTEGRATIONS.md`](./INTEGRATIONS.md) — pi, claude code, codex, opencode, hermes integrations
+- [configuration](https://herdr.dev/docs/configuration/) — keybindings, themes, notifications, environment variables
+- [integrations](https://herdr.dev/docs/integrations/) — pi, claude code, codex, opencode, hermes integrations
- [`SKILL.md`](./SKILL.md) — reusable agent skill
-- [`SOCKET_API.md`](./SOCKET_API.md) — socket protocol and cli reference
+- [socket api](https://herdr.dev/docs/socket-api/) — socket protocol and cli reference
## agent instructions
diff --git a/docs/next/SOCKET_API.md b/docs/next/SOCKET_API.md
deleted file mode 100644
index 7ffb276e..00000000
--- a/docs/next/SOCKET_API.md
+++ /dev/null
@@ -1,1112 +0,0 @@
-# herdr socket api
-
-herdr exposes a local unix socket api for scripts, tools, and coding agents that want to control a running herdr instance or subscribe to events.
-
-if you are teaching an agent that is already running inside herdr, start with [`SKILL.md`](./SKILL.md). use this document when you want the direct protocol, or when you want the cli wrapper reference for the commands that sit on top of it.
-
-## choose your integration layer
-
-there are three practical ways to integrate with herdr:
-
-- **agent skill** — [`SKILL.md`](./SKILL.md). best when an agent inside herdr just needs to learn the workflow quickly.
-- **cli wrappers** — `herdr server stop`, `herdr workspace ...`, `herdr tab ...`, `herdr agent ...`, `herdr pane ...`, `herdr wait ...`. best for shell scripts and simple orchestration.
-- **raw socket api** — best when you want direct request/response control or long-lived event subscriptions.
-
-these layers are intentionally stacked on top of the same control surface.
-
-important difference: `pane.run` and `wait agent-status` are **cli conveniences**, not raw socket methods.
-
-## transport
-
-- transport: unix domain socket
-- encoding: newline-delimited json
-- request/response: send one json request per line, read one json response per line
-- subscriptions: send `events.subscribe`, receive an ack, then keep the same connection open and continue reading pushed events
-
-named sessions are runtime/socket namespaces, not replacements for herdr workspaces. each named session has its own server sockets and persistent runtime state while config remains global.
-
-socket path resolution order:
-
-1. explicit `herdr --session <name>`:
- `$XDG_CONFIG_HOME/herdr/sessions/<name>/herdr.sock` or `$HOME/.config/herdr/sessions/<name>/herdr.sock`
-2. `HERDR_SOCKET_PATH`
-3. `HERDR_SESSION=<name>`:
- `$XDG_CONFIG_HOME/herdr/sessions/<name>/herdr.sock` or `$HOME/.config/herdr/sessions/<name>/herdr.sock`
-4. default session path:
- `$XDG_CONFIG_HOME/herdr/herdr.sock` or `$HOME/.config/herdr/herdr.sock`
-
-this means `HERDR_SOCKET_PATH` remains an exact low-level socket override, but an explicit cli `--session <name>` still wins when a command runs inside a pane that inherited `HERDR_SOCKET_PATH`.
-
-session names may contain ASCII letters, numbers, `.`, `_`, and `-`. `default` is reserved for the default session. use `herdr session list`, `herdr session attach <name>`, `herdr session stop <name>`, and `herdr session delete <name>` to inspect and manage session namespaces. session commands print human-readable output by default; pass `--json` for machine-readable output. `session delete` refuses running sessions and does not delete the default session.
-
-## request and response envelopes
-
-all socket requests use this envelope:
-
-```json
-{
- "id": "req_1",
- "method": "ping",
- "params": {}
-}
-```
-
-successful responses look like:
-
-```json
-{
- "id": "req_1",
- "result": {
- "type": "pong",
- "version": "0.1.2",
- "protocol": 2
- }
-}
-```
-
-errors look like:
-
-```json
-{
- "id": "req_1",
- "error": {
- "code": "pane_not_found",
- "message": "pane 1-99 not found"
- }
-}
-```
-
-## ids and numbering
-
-workspace ids are opaque, stable ids like:
-
-- `w64e95948145ed1`
-- `w64e95948146a82`
-
-pane ids are workspace-scoped and stable across workspace reorder:
-
-- `w64e95948145ed1-1`
-- `w64e95948145ed1-2`
-- `w64e95948146a82-1`
-
-that means:
-
-- workspace id = stable workspace identity
-- pane number = compact pane number within that workspace
-
-workspace ids are durable for the life of the workspace and survive display reordering. pane numbers are still compact public numbers, so if a pane closes, higher pane numbers in that same workspace compact down.
-
-tabs are first-class socket api objects now.
-
-- tab ids look like `w64e95948145ed1:1`, `w64e95948145ed1:2`
-- workspace id = stable workspace identity
-- tab number = tab number within that workspace
-- pane ids still stay workspace-scoped like `w64e95948145ed1-2` rather than becoming `workspace-tab-pane` triples
-
-for backward compatibility, requests also accept the older positional forms like `1`, `1:2`, and `1-2` as shorthand for the current session order. responses use the stable ids.
-
-## core objects
-
-`workspace_info` responses contain objects like:
-
-```json
-{
- "workspace_id": "w64e95948145ed1",
- "number": 1,
- "label": "herdr",
- "focused": true,
- "pane_count": 1,
- "tab_count": 1,
- "active_tab_id": "w64e95948145ed1:1",
- "agent_status": "unknown"
-}
-```
-
-`tab_info` responses contain objects like:
-
-```json
-{
- "tab_id": "w64e95948145ed1:1",
- "workspace_id": "w64e95948145ed1",
- "number": 1,
- "label": "1",
- "focused": true,
- "pane_count": 1,
- "agent_status": "unknown"
-}
-```
-
-`pane_info` responses contain objects like:
-
-```json
-{
- "pane_id": "w64e95948145ed1-1",
- "terminal_id": "term_64e95948145ed1",
- "workspace_id": "w64e95948145ed1",
- "tab_id": "w64e95948145ed1:1",
- "focused": true,
- "cwd": "/home/can/Projects/herdr",
- "label": "reviewer",
- "agent": "pi",
- "agent_status": "working",
- "custom_status": "indexing",
- "revision": 0
-}
-```
-
-`terminal_id` is an opaque terminal identity. during the pane-backed transition each pane has one terminal id, but clients should not derive it from the pane id.
-
-`label` is an optional manual pane name set through `pane.rename`.
-
-`agent_info` responses contain a terminal-facing view of a live agent terminal:
-
-```json
-{
- "terminal_id": "term_64e95948145ed1",
- "name": "reviewer",
- "agent": "pi",
- "agent_status": "working",
- "workspace_id": "w64e95948145ed1",
- "tab_id": "w64e95948145ed1:1",
- "pane_id": "w64e95948145ed1-1",
- "focused": true,
- "cwd": "/home/can/Projects/herdr",
- "revision": 0
-}
-```
-
-`name` is an optional unique agent alias set by `agent.start` or `agent.rename`. it is separate from the pane `label`: `pane.rename` names a terminal/pane for UI purposes, while `agent.rename` declares or changes the agent-facing name and rejects duplicate active agent names.
-
-`agent` is an optional display label string.
-
-- when herdr detects a built-in agent, this is that built-in name like `pi` or `claude`
-- when a hook or plugin reports a custom agent through `pane.report_agent`, this can be any non-empty label like `hermes`
-- when no agent identity is known, it is omitted
-
-`custom_status` on `pane_info` and agent-status events is an optional hook-owned display label. It is present only while hook authority is active. It does not change `agent_status`, `done` semantics, waits, notifications, or workspace/tab rollup priority.
-
-`pane_read` responses contain objects like:
-
-```json
-{
- "pane_id": "w64e95948145ed1-1",
- "workspace_id": "w64e95948145ed1",
- "tab_id": "w64e95948145ed1:1",
- "source": "recent",
- "format": "text",
- "text": "...",
- "revision": 0,
- "truncated": false
-}
-```
-
-`agent_status` is the public agent field:
-
-- `idle`
-- `working`
-- `blocked`
-- `done`
-- `unknown`
-
-`done` means the agent has finished, but you have not looked at that finished pane yet.
-
-## methods at a glance
-
-| method | purpose | success result type |
-|---|---|---|
-| `ping` | health check / version | `pong` |
-| `server.stop` | gracefully stop the running background server | `ok` |
-| `workspace.list` | list workspaces | `workspace_list` |
-| `workspace.get` | inspect one workspace | `workspace_info` |
-| `workspace.create` | create a workspace | `workspace_info` |
-| `workspace.focus` | focus a workspace | `workspace_info` |
-| `workspace.rename` | rename a workspace | `workspace_info` |
-| `workspace.close` | close a workspace | `ok` |
-| `tab.list` | list tabs, optionally filtered by workspace | `tab_list` |
-| `tab.get` | inspect one tab | `tab_info` |
-| `tab.create` | create a tab in a workspace | `tab_info` |
-| `tab.focus` | focus a tab | `tab_info` |
-| `tab.rename` | rename a tab | `tab_info` |
-| `tab.close` | close a tab | `ok` |
-| `agent.list` | list terminal-backed agents | `agent_list` |
-| `agent.get` | inspect one agent by terminal id, unique agent name, detected agent label, or pane id | `agent_info` |
-| `agent.read` | read output from one agent terminal | `pane_read` |
-| `agent.send` | send literal text to one agent terminal | `ok` |
-| `agent.rename` | set or clear the unique agent name | `agent_info` |
-| `agent.focus` | show the agent terminal in the TUI | `agent_info` |
-| `agent.start` | start a named agent terminal from argv | `agent_started` |
-| `pane.list` | list panes, optionally filtered by workspace | `pane_list` |
-| `pane.get` | inspect one pane | `pane_info` |
-| `pane.rename` | set or clear a manual pane label | `pane_info` |
-| `pane.read` | read pane output | `pane_read` |
-| `pane.split` | split a pane and create a sibling pane | `pane_info` |
-| `pane.send_text` | send literal text without Enter | `ok` |
-| `pane.send_keys` | send keypresses like `Enter` | `ok` |
-| `pane.send_input` | send literal text plus keypresses in order | `ok` |
-| `pane.report_agent` | report hook-authoritative agent label and state for a pane | `ok` |
-| `pane.clear_agent_authority` | clear hook-authoritative agent state for a pane | `ok` |
-| `pane.release_agent` | release a pane from the reported agent back to shell state | `ok` |
-| `pane.close` | close a pane | `ok` |
-| `pane.wait_for_output` | one-shot blocking wait for text | `output_matched` |
-| `events.subscribe` | start a long-lived subscription stream | `subscription_started` ack |
-
-### `server.stop`
-
-request:
-
-```json
-{
- "id": "req_stop",
- "method": "server.stop",
- "params": {}
-}
-```
-
-returns `ok` and asks the running background server to shut down cleanly.
-
-this is the explicit server-level shutdown path for persistence mode. normal in-app quit actions detach the current client instead of sending this request.
-
-## workspace methods
-
-### `workspace.list`
-
-request:
-
-```json
-{
- "id": "req_list",
- "method": "workspace.list",
- "params": {}
-}
-```
-
-returns `workspace_list` with zero or more workspace objects.
-
-### `workspace.get`
-
-params:
-
-```json
-{
- "workspace_id": "1"
-}
-```
-
-returns `workspace_info` for one workspace.
-
-### `workspace.create`
-
-params:
-
-```json
-{
- "cwd": "/home/can/Projects/herdr",
- "focus": true
-}
-```
-
-notes:
-
-- `cwd` is optional
-- if `cwd` is omitted, herdr uses its current working directory and falls back to `/` if needed
-- `focus` is optional in raw socket requests and defaults to `false`
-- the cli wrapper also defaults to no focus; pass `--focus` to switch to the new workspace
-
-example response:
-
-```json
-{
- "id": "req_create",
- "result": {
- "type": "workspace_info",
- "workspace": {
- "workspace_id": "1",
- "number": 1,
- "label": "herdr",
- "focused": true,
- "pane_count": 1,
- "tab_count": 1,
- "active_tab_id": "1:1",
- "agent_status": "unknown"
- }
- }
-}
-```
-
-### `workspace.focus`
-
-params:
-
-```json
-{
- "workspace_id": "1"
-}
-```
-
-returns the focused workspace as `workspace_info`.
-
-### `workspace.rename`
-
-params:
-
-```json
-{
- "workspace_id": "1",
- "label": "api"
-}
-```
-
-returns updated `workspace_info`.
-
-### `workspace.close`
-
-params:
-
-```json
-{
- "workspace_id": "1"
-}
-```
-
-returns:
-
-```json
-{
- "id": "req_close",
- "result": {
- "type": "ok"
- }
-}
-```
-
-## tab methods
-
-### `tab.list`
-
-request with no filter:
-
-```json
-{
- "id": "req_tabs",
- "method": "tab.list",
- "params": {}
-}
-```
-
-request filtered to one workspace:
-
-```json
-{
- "id": "req_tabs_ws",
- "method": "tab.list",
- "params": {
- "workspace_id": "1"
- }
-}
-```
-
-returns `tab_list`.
-
-### `tab.get`
-
-params:
-
-```json
-{
- "tab_id": "1:2"
-}
-```
-
-returns `tab_info`.
-
-### `tab.create`
-
-params:
-
-```json
-{
- "workspace_id": "1",
- "cwd": "/home/can/Projects/herdr",
- "focus": true
-}
-```
-
-notes:
-
-- `workspace_id` is optional and defaults to the active workspace
-- `cwd` is optional; if omitted, herdr uses the focused pane cwd in that workspace when available
-- `focus` is optional in raw socket requests and defaults to `false`
-- the cli wrapper also defaults to no focus; pass `--focus` to switch to the new tab
-
-returns `tab_info` for the new tab.
-
-### `tab.focus`
-
-params:
-
-```json
-{
- "tab_id": "1:2"
-}
-```
-
-returns focused `tab_info`.
-
-### `tab.rename`
-
-params:
-
-```json
-{
- "tab_id": "1:2",
- "label": "logs"
-}
-```
-
-returns updated `tab_info`.
-
-### `tab.close`
-
-params:
-
-```json
-{
- "tab_id": "1:2"
-}
-```
-
-returns `ok`. the last tab in a workspace cannot be closed.
-
-## pane methods
-
-### `pane.list`
-
-request with no filter:
-
-```json
-{
- "id": "req_panes",
- "method": "pane.list",
- "params": {}
-}
-```
-
-request filtered to one workspace:
-
-```json
-{
- "id": "req_panes_ws",
- "method": "pane.list",
- "params": {
- "workspace_id": "1"
- }
-}
-```
-
-returns `pane_list`.
-
-### `pane.get`
-
-params:
-
-```json
-{
- "pane_id": "1-1"
-}
-```
-
-returns `pane_info`.
-
-### `pane.rename`
-
-params:
-
-```json
-{
- "pane_id": "1-1",
- "label": "reviewer"
-}
-```
-
-send `label: null` or omit `label` to clear the manual pane label.
-
-returns `pane_info`.
-
-### `pane.read`
-
-params:
-
-```json
-{
- "pane_id": "1-1",
- "source": "recent",
- "lines": 80,
- "format": "text",
- "strip_ansi": true
-}
-```
-
-notes:
-
-- `source` is required and must be `visible`, `recent`, or `recent_unwrapped`
-- `lines` is optional
-- current implementation defaults to `80` lines when `lines` is omitted and caps reads at `1000`
-- `format` defaults to `text`; use `ansi` for a rendered VT/ANSI snapshot with styles preserved
-- `strip_ansi` defaults to `true` and is kept for compatibility
-
-`source` meanings:
-
-- `visible` — current viewport
-- `recent` — recent scrollback text
-- `recent_unwrapped` — recent scrollback text with soft wraps joined
-
-example response:
-
-```json
-{
- "id": "req_read",
- "result": {
- "type": "pane_read",
- "read": {
- "pane_id": "1-1",
- "workspace_id": "1",
- "tab_id": "1:1",
- "source": "recent",
- "format": "text",
- "text": "...",
- "revision": 0,
- "truncated": false
- }
- }
-}
-```
-
-### `pane.split`
-
-params:
-
-```json
-{
- "target_pane_id": "1-1",
- "direction": "right",
- "focus": true
-}
-```
-
-notes:
-
-- `direction` must be `right` or `down`
-- `cwd` is optional
-- `focus` is optional in raw socket requests and defaults to `false`
-- the cli wrapper also defaults to no focus; pass `--focus` to switch to the new pane
-
-returns `pane_info` for the new pane.
-
-### `pane.send_text`
-
-params:
-
-```json
-{
- "pane_id": "1-1",
- "text": "bun run dev"
-}
-```
-
-this sends literal text only. it does **not** press Enter.
-
-### `pane.send_keys`
-
-params:
-
-```json
-{
- "pane_id": "1-1",
- "keys": ["Enter"]
-}
-```
-
-use this after `pane.send_text` when you want to submit a command.
-
-### `pane.send_input`
-
-params:
-
-```json
-{
- "pane_id": "1-1",
- "text": "bun run dev",
- "keys": ["Enter"]
-}
-```
-
-this sends text plus encoded keypresses in order within one request. when bracketed paste is enabled in the pane, the text portion is sent as a paste payload before the keys. use this when you need `text + Enter` to behave more like a real keypress sequence than `pane.send_text` with a literal trailing `\r`.
-
-`text` and `keys` are both optional, but at least one should usually be present.
-
-### `pane.report_agent`
-
-use this when an agent hook or plugin wants to report a semantic state directly over the socket api.
-
-params:
-
-```json
-{
- "pane_id": "1-1",
- "source": "custom:hermes",
- "agent": "hermes",
- "state": "working",
- "message": "running tools",
- "custom_status": "indexing"
-}
-```
-
-notes:
-
-- `source` is required and identifies the reporting integration instance
-- `agent` is required and may be any non-empty label string
-- built-in names like `pi` are normalized to their public label form
-- custom labels like `hermes` are accepted as-is
-- while this authority is active, the reported `agent` and `state` override heuristic display for that pane
-- process detection still owns pane liveness and fallback when hook authority is cleared or released
-- `message` is optional metadata for the reporting integration
-- `custom_status` is an optional short display label such as `scheduled`, `indexing`, or `stuck`
-- `custom_status` is visual-only; use `state` for semantic behavior like working or blocked
-- omitting `custom_status` clears any previous custom status from this reporting source
-
-returns `ok`.
-
-### `pane.clear_agent_authority`
-
-params:
-
-```json
-{
- "pane_id": "1-1",
- "source": "custom:hermes"
-}
-```
-
-notes:
-
-- `source` is optional
-- when `source` is omitted, any hook authority for that pane is cleared
-- when `source` is present, only that reporting source is cleared
-
-returns `ok`.
-
-### `pane.release_agent`
-
-use this when the reported agent is exiting cleanly and wants herdr to drop agent identity immediately instead of waiting for fallback detection.
-
-params:
-
-```json
-{
- "pane_id": "1-1",
- "source": "custom:hermes",
- "agent": "hermes"
-}
-```
-
-notes:
-
-- `agent` uses the same non-empty label rules as `pane.report_agent`
-- this clears the pane's effective agent identity immediately when the source and label match the active authority
-- for built-in detected agents, herdr also applies its normal short reacquire suppression during graceful release
-
-returns `ok`.
-
-### `pane.close`
-
-params:
-
-```json
-{
- "pane_id": "1-2"
-}
-```
-
-returns `ok`.
-
-## waits
-
-### `pane.wait_for_output`
-
-this is the direct socket-side one-shot blocking wait.
-
-params:
-
-```json
-{
- "pane_id": "1-1",
- "source": "recent",
- "lines": 200,
- "match": { "type": "substring", "value": "ready" },
- "timeout_ms": 30000,
- "strip_ansi": true
-}
-```
-
-matcher forms:
-
-```json
-{ "type": "substring", "value": "ready" }
-```
-
-```json
-{ "type": "regex", "value": "server.*ready" }
-```
-
-notes:
-
-- `source` must be `visible`, `recent`, or `recent_unwrapped`
-- `lines` is optional
-- `timeout_ms` is optional
-- `strip_ansi` defaults to `true`
-- for `source = "recent"`, output matching uses unwrapped recent terminal text so soft wraps do not break matches
-- `source = "recent_unwrapped"` is also available on `pane.read` when you want to inspect the same unwrapped transcript directly
-- on success you get `output_matched`
-- on timeout you get an error response with code `timeout`
-
-example success response:
-
-```json
-{
- "id": "req_wait",
- "result": {
- "type": "output_matched",
- "pane_id": "1-1",
- "revision": 0,
- "matched_line": "server ready",
- "read": {
- "pane_id": "1-1",
- "workspace_id": "1",
- "tab_id": "1:1",
- "source": "recent_unwrapped",
- "text": "...server ready...",
- "revision": 0,
- "truncated": false
- }
- }
-}
-```
-
-## subscriptions
-
-`events.subscribe` is the long-lived pubsub entrypoint.
-
-you send a subscribe request once, get an ack on the same connection, and then keep reading newline-delimited json events from that same socket.
-
-### subscription ack
-
-```json
-{
- "id": "sub_1",
- "result": {
- "type": "subscription_started"
- }
-}
-```
-
-### supported subscriptions
-
-base lifecycle subscriptions:
-
-- `workspace.created`
-- `workspace.closed`
-- `workspace.focused`
-- `tab.created`
-- `tab.closed`
-- `tab.focused`
-- `tab.renamed`
-- `pane.created`
-- `pane.closed`
-- `pane.focused`
-- `pane.exited`
-- `pane.agent_detected`
-
-parameterized subscriptions:
-
-- `pane.output_matched`
-- `pane.agent_status_changed`
-
-### event naming rule
-
-this part matters because the pushed event names are **not all shaped the same**.
-
-- when you subscribe to a **base lifecycle event**, the pushed `event` value uses snake_case with underscores:
- - subscribe with `workspace.created`
- - receive `workspace_created`
-- when you subscribe to a **parameterized subscription**, the pushed `event` value keeps the dotted name:
- - subscribe with `pane.output_matched`
- - receive `pane.output_matched`
-
-examples below show both forms.
-
-### example: subscribe to lifecycle events
-
-request:
-
-```json
-{
- "id": "sub_life",
- "method": "events.subscribe",
- "params": {
- "subscriptions": [
- { "type": "workspace.created" },
- { "type": "workspace.focused" },
- { "type": "tab.created" },
- { "type": "tab.focused" },
- { "type": "tab.renamed" },
- { "type": "tab.closed" },
- { "type": "pane.created" },
- { "type": "pane.focused" },
- { "type": "pane.agent_detected" },
- { "type": "pane.closed" },
- { "type": "workspace.closed" }
- ]
- }
-}
-```
-
-example pushed event:
-
-```json
-{
- "event": "workspace_created",
- "data": {
- "workspace": {
- "workspace_id": "1",
- "number": 1,
- "label": "herdr",
- "focused": true,
- "pane_count": 1,
- "tab_count": 1,
- "active_tab_id": "1:1",
- "agent_status": "unknown"
- }
- }
-}
-```
-
-### example: subscribe to output matches and agent status changes
-
-request:
-
-```json
-{
- "id": "sub_1",
- "method": "events.subscribe",
- "params": {
- "subscriptions": [
- {
- "type": "pane.output_matched",
- "pane_id": "1-1",
- "source": "recent",
- "lines": 200,
- "match": { "type": "substring", "value": "ready" }
- },
- {
- "type": "pane.agent_status_changed",
- "pane_id": "1-1",
- "agent_status": "done"
- }
- ]
- }
-}
-```
-
-notes:
-
-- `pane.output_matched` supports `source`, optional `lines`, matcher config, and optional `strip_ansi`
-- `pane.agent_status_changed` accepts an optional `agent_status` filter; if omitted, any status transition for that pane can match
-
-example pushed `pane.output_matched` event:
-
-```json
-{
- "event": "pane.output_matched",
- "data": {
- "pane_id": "1-1",
- "matched_line": "server ready",
- "read": {
- "pane_id": "1-1",
- "workspace_id": "1",
- "tab_id": "1:1",
- "source": "recent_unwrapped",
- "text": "...server ready...",
- "revision": 0,
- "truncated": false
- }
- }
-}
-```
-
-example pushed `pane.agent_status_changed` event:
-
-```json
-{
- "event": "pane.agent_status_changed",
- "data": {
- "pane_id": "1-1",
- "workspace_id": "1",
- "agent_status": "done",
- "agent": "pi",
- "custom_status": "scheduled"
- }
-}
-```
-
-`agent` in pushed events follows the same rules as `pane_info.agent`: it may be a built-in detected name, a custom hook-reported label, or omitted. `custom_status` may be included when the pane has hook-owned custom status metadata at the time of the semantic status transition.
-## cli wrappers
-
-these commands provide the shell-facing control surface. most command groups talk to the local socket; `status client` only inspects the local executable.
-
-### command groups
-
-status commands:
-
-```text
-herdr status
-herdr status server
-herdr status client
-```
-
-`herdr -V` and `herdr --version` print the local executable version without contacting the server. `herdr status` compares that local executable with the running server when one is reachable.
-
-workspace commands:
-
-```text
-herdr workspace list
-herdr workspace create [--cwd PATH] [--label TEXT] [--focus] [--no-focus]
-herdr workspace get <workspace_id>
-herdr workspace focus <workspace_id>
-herdr workspace rename <workspace_id> <label>
-herdr workspace close <workspace_id>
-```
-
-tab commands:
-
-```text
-herdr tab list [--workspace <workspace_id>]
-herdr tab create [--workspace <workspace_id>] [--cwd PATH] [--label TEXT] [--focus] [--no-focus]
-herdr tab get <tab_id>
-herdr tab focus <tab_id>
-herdr tab rename <tab_id> <label>
-herdr tab close <tab_id>
-```
-
-agent commands:
-
-```text
-herdr agent list
-herdr agent get <target>
-herdr agent read <target> [--source visible|recent|recent-unwrapped] [--lines N] [--format text|ansi] [--ansi]
-herdr agent send <target> <text>
-herdr agent rename <target> <name>|--clear
-herdr agent focus <target>
-herdr agent wait <target> --status <idle|working|blocked|unknown> [--timeout MS]
-herdr agent attach <target> [--takeover]
-herdr agent start <name> [--cwd PATH] [--workspace ID] [--tab ID] [--split right|down] [--focus|--no-focus] -- <argv...>
-```
-
-agent targets accept terminal ids, unique agent names, detected/reported agent labels, and legacy pane ids. `agent list` shows terminals with explicit agent identity, detected/reported agent identity, or `agent.start` launch metadata. `pane.rename` does not make a plain terminal appear in `agent list`; use `agent.rename` for agent identity.
-
-terminal commands:
-
-```text
-herdr terminal attach <terminal_id> [--takeover]
-```
-
-`terminal attach` is the low-level direct attach primitive. it streams the current rendered terminal state first, then live ANSI frames. direct attach has one writable input/resize owner; a second direct attach fails unless you pass `--takeover`. detach with `ctrl+b q`; send a literal `ctrl+b` with `ctrl+b ctrl+b`.
-
-pane commands:
-
-```text
-herdr pane list [--workspace <workspace_id>]
-herdr pane get <pane_id>
-herdr pane rename <pane_id> <label>|--clear
-herdr pane read <pane_id> [--source visible|recent|recent-unwrapped] [--lines N] [--format text|ansi] [--ansi]
-herdr pane split <pane_id> --direction right|down [--cwd PATH] [--focus] [--no-focus]
-herdr pane close <pane_id>
-herdr pane send-text <pane_id> <text>
-herdr pane send-keys <pane_id> <key> [key ...]
-herdr pane report-agent <pane_id> --source ID --agent LABEL --state idle|working|blocked|unknown [--message TEXT] [--custom-status TEXT] [--seq N]
-herdr pane run <pane_id> <command>
-```
-
-wait commands:
-
-```text
-herdr wait output <pane_id> --match <text> [--source visible|recent|recent-unwrapped] [--lines N] [--timeout MS] [--regex] [--raw]
-herdr wait agent-status <pane_id> --status <idle|working|blocked|done|unknown> [--timeout MS]
-```
-
-### cli behavior notes
-
-- `status` prints local client version/protocol, running server version/protocol when reachable, socket path, compatibility, and whether a restart is needed
-- `status server` prints only the running server side; if no server is reachable it exits successfully and prints `status: not running`
-- `status client` prints only the local executable version/protocol and binary path without contacting the server
-- `workspace create` keeps focus where it is by default; pass `--focus` to switch to the new workspace
-- `workspace create` without `--label` keeps the default cwd-based workspace naming
-- `workspace create --label` applies the custom workspace name immediately
-- `workspace create` returns `result.workspace`, `result.tab`, and `result.root_pane`
-- `tab create` keeps focus where it is by default; pass `--focus` to switch to the new tab
-- `tab create` without `--label` keeps the default numbered tab naming
-- `tab create --label` applies the custom tab name immediately
-- `tab create` returns `result.tab` and `result.root_pane`
-- `agent list` shows agent terminals, not arbitrary named panes
-- `agent wait --status idle` is the CLI completion wait; it treats the UI-only `done` attention state as satisfying idle
-- `agent focus` switches the TUI workspace/tab/pane focus; it is not direct terminal attach
-- `agent attach` attaches the current terminal directly to the agent terminal stream
-- `agent start <name> -- <argv...>` starts a named agent terminal and returns `result.agent` plus `result.argv`
-- `pane split` keeps focus where it is by default; pass `--focus` to switch to the new pane
-- `pane read` prints **text**, not json
-- `pane read --format ansi` and `pane read --ansi` print a rendered ANSI snapshot with colors/styles preserved
-- `pane read --source recent-unwrapped` returns recent terminal text with soft wraps joined back together
-- `pane send-text`, `pane send-keys`, `pane report-agent`, and `pane run` print nothing on success
-- list/get/create/split/wait commands print json on success
-- `pane run` is a convenience wrapper for `pane.send_input` with the command text followed by a real `Enter` keypress
-- `wait agent-status` is a cli convenience built on top of event subscriptions
-- use it when you want the same `done` / `idle` distinction the UI shows
-- `--raw` is a legacy alias for ANSI formatted `pane read` output and still disables ansi stripping for `wait output`
-- `wait output --source recent` matches against unwrapped recent terminal text by default, so pane width and soft wrapping do not break matches
-
-### cli examples
-
-create a workspace, split a pane, run a server, and wait for readiness:
-
-```bash
-herdr workspace create --cwd /path/to/project --label "api server"
-herdr pane split 1-1 --direction right --no-focus
-herdr pane run 1-2 "npm run dev"
-herdr wait output 1-2 --match "ready" --timeout 30000
-```
-
-wait for an agent by name to become idle for CLI automation:
-
-```bash
-herdr agent wait reviewer --status idle --timeout 60000
-```
-
-wait for a pane-level UI attention state:
-
-```bash
-herdr wait agent-status 1-1 --status done --timeout 60000
-```
-
-inspect another pane's output:
-
-```bash
-herdr pane read 1-1 --source recent --lines 80
-```
-
-## behavior notes and gotchas
-
-- `pane.send_text` sends literal text only. if you want to execute a command, follow it with `pane.send_keys` and `Enter`, use `pane.send_input` for ordered `text + keypress` input, or use cli `pane run`, which sends the text and then a real Enter key in one request.
-- `pane.read` and `pane.wait_for_output` strip ansi by default.
-- `pane.output_matched` subscriptions fire on transitions into a matching state; they do not repeatedly spam the same still-visible match on every poll.
-- closing the socket connection ends the subscription.
-- there is no separate event transport.
-- the same herdr process can serve regular request/response calls and long-lived subscription connections at the same time.
diff --git a/justfile b/justfile
index bfa8cd80..3c56435e 100644
--- a/justfile
+++ b/justfile
@@ -17,6 +17,7 @@ ci: lint
# Check formatting + run unit tests + maintenance script tests
check: ci
python3 -m unittest scripts.test_changelog scripts.test_vendor_libghostty_vt
+ @echo "docs reminder: if this changes user-facing behavior, make sure the relevant release docs are updated or called out before release."
# Install repo-local git hooks
install-hooks:
@@ -36,14 +37,23 @@ website-build:
build-libghostty-vt:
scripts/build_vendored_libghostty_vt.sh
-# Check that public docs and changelog have been finalized from docs/next before release
+# Check that release docs and changelog have been finalized from docs/next before release
release-docs-check:
- @for file in README.md CONFIGURATION.md INTEGRATIONS.md SOCKET_API.md CHANGELOG.md; do \
+ @for file in README.md CHANGELOG.md; do \
if ! diff -u "$file" "docs/next/$file"; then \
echo "error: $file differs from docs/next/$file; finalize release docs before releasing"; \
exit 1; \
fi; \
done
+ @for file in CONFIGURATION.md INTEGRATIONS.md SOCKET_API.md; do \
+ if [ -e "$file" ]; then \
+ echo "error: $file was replaced by website docs; remove the root copy"; \
+ exit 1; \
+ fi; \
+ done
+ @test -f website/src/content/docs/configuration.mdx
+ @test -f website/src/content/docs/integrations.mdx
+ @test -f website/src/content/docs/socket-api.mdx
# Finalize changelog, bump version, commit, tag, push, and trigger the GitHub Release workflow (usage: just release 0.1.1)
release version:
diff --git a/website/src/content/docs/agents.mdx b/website/src/content/docs/agents.mdx
index e3d2bc0c..ae21defe 100644
--- a/website/src/content/docs/agents.mdx
+++ b/website/src/content/docs/agents.mdx
@@ -18,7 +18,6 @@ Automatic detection works out of the box for common coding agents.
| Amp | yes | yes | yes |
| OpenCode | yes | yes | yes |
| Grok CLI | yes | yes | yes |
-| Hermes Agent | yes | yes | yes |
| GitHub Copilot CLI | yes | yes | yes |
Detected but less thoroughly tested: Gemini CLI, Cursor Agent, Cline, and Kimi.
@@ -52,7 +51,6 @@ herdr integration install pi
herdr integration install claude
herdr integration install codex
herdr integration install opencode
-herdr integration install hermes
```
Check installed integration versions:
diff --git a/website/src/content/docs/cli-reference.mdx b/website/src/content/docs/cli-reference.mdx
index 57f495c1..78487b6a 100644
--- a/website/src/content/docs/cli-reference.mdx
+++ b/website/src/content/docs/cli-reference.mdx
@@ -165,12 +165,10 @@ herdr integration install pi
herdr integration install claude
herdr integration install codex
herdr integration install opencode
-herdr integration install hermes
herdr integration uninstall pi
herdr integration uninstall claude
herdr integration uninstall codex
herdr integration uninstall opencode
-herdr integration uninstall hermes
herdr integration status [--outdated-only]
```
diff --git a/website/src/content/docs/integrations.mdx b/website/src/content/docs/integrations.mdx
index 01e72e16..a447688c 100644
--- a/website/src/content/docs/integrations.mdx
+++ b/website/src/content/docs/integrations.mdx
@@ -1,6 +1,6 @@
---
title: Integrations
-description: Install Herdr integrations for Pi, Claude Code, Codex, OpenCode, and Hermes Agent.
+description: Install Herdr integrations for Pi, Claude Code, Codex, and OpenCode.
---
Herdr detects supported agents automatically. Integrations make that detection more precise by reporting semantic state directly to Herdr.
@@ -14,7 +14,6 @@ herdr integration install pi
herdr integration install claude
herdr integration install codex
herdr integration install opencode
-herdr integration install hermes
```
## Uninstall integrations
@@ -24,7 +23,6 @@ herdr integration uninstall pi
herdr integration uninstall claude
herdr integration uninstall codex
herdr integration uninstall opencode
-herdr integration uninstall hermes
```
## How Herdr uses integrations
@@ -85,17 +83,6 @@ herdr integration install opencode
The plugin reports semantic state while OpenCode runs inside a Herdr pane.
-## Hermes Agent
-
-Install the Hermes Agent plugin:
-
-```bash
-herdr integration install hermes
-```
-
-Herdr writes `~/.hermes/plugins/herdr-agent-state/` and enables `herdr-agent-state` in `~/.hermes/config.yaml`. Restart Hermes after installing so the plugin loads.
-
-The plugin reports lifecycle, tool, and approval state while Hermes runs inside a Herdr pane. Native screen heuristics remain available when the plugin is not installed.
## Custom status labels