AI-dev atlas Β· agent runtime
A background server that owns your terminals, watches which coding agent is stuck, and keeps every pane alive after the laptop lid closes. One Rust binary, no model in the loop.
herdr is a body, not a brain. It is a terminal workspace manager that runs as a background server: your shells and your coding agents live inside its processes, not inside your terminal window. Close the laptop, drop the wifi, walk to another machine β the agents keep running and you reattach to the same session. On top of that it does one clever thing: it watches the bottom of each terminal's screen and works out whether the agent inside is working, blocked, done, or idle, then rolls that upward so you can see at a glance which of your eight parallel agents is the one waiting on you.
It runs agents it did not write β Claude Code, Codex, Cursor, OpenCode, Droid, Grok and a dozen more β without wrapping or modifying them. And the same CLI you type is the CLI an agent inside a pane can type, so one agent can split a pane, start a second agent, hand it a prompt, and block until it settles.
| Shared term | herdr calls it | Where it lives |
|---|---|---|
| body vs brain | runtime / "the runtime your coding agents live on" | herdr is pure body β it has no memory layer at all |
| dispatcher | status authority | detect/mod.rs β picks which signal may author state |
| filing gate | manifest rule / priority ladder | detect/manifest.rs |
| always-on | autodetect loop | server/autodetect.rs |
| skill | agent skill file | skills/herdr/SKILL.md |
| gate vs advisory | proposed term β herdr's version is state authority: exactly one signal per pane may say blocked | pane/agent_detection.rs |
| artifact handoff | live handoff β but of PTYs, not files | server/handoff.rs |
What hosts it: your own machine, or any Linux/macOS box you can SSH to. One Rust binary, no Electron, no daemon you configure.
What plugs into it: the coding agents themselves (19 recognised, 16 with official hook/plugin integrations), plus user plugins declared in a herdr-plugin.toml.
What is NOT part of this repo: any memory, retrieval, planning, review, or model-calling layer. herdr never calls an LLM. It owns terminals and reports state; everything intelligent happens inside the panes.
One honest caveat. herdr's cleverness is regex over terminal bytes. When an agent ships a new permission dialog herdr has not seen, that agent shows idle instead of blocked β the docs say so plainly: "unusual new agent prompts may initially show as idle until Herdr learns that screen shape." Remote manifest updates shrink that lag to hours instead of a release cycle, but a script that trusts agent wait --until idle can be told "ready" by an agent that is actually sitting on an approval prompt.
The repo rendered as answers to the library's canonical questions. IDs are the cross-atlas comparison keys. R# is a proposed new bank β Agent Runtime β parked as DRAFT in DECISIONS.md.
choseThe live OS processes are the truth. herdr persists only the session's shape β workspaces, tabs, panes, cwd, layout, focus β to session.json in its data dir. Terminal content is not stored at all by default.
why"This is the strongest persistence path because the original processes never stop." herdr's durability answer is "don't stop the process," not "replay the log."
trade-offIf the server does stop, everything except layout is gone. Panes come back as fresh shells in their saved directories; recovering the conversation needs either the agent's own session file or an opt-in screen-history cache.
linkspersist/io.rs Β· persist/snapshot.rs Β· docs/session-state.mdx
choseLocal-only transport β a Unix domain socket (Windows named pipe) under your config dir, no network listener ever. Remote access is plain OpenSSH. Agents self-gate on HERDR_ENV=1. Screen history is off by default. Plugins are explicitly not sandboxed.
whyOn plugins: "Herdr validates the manifest and keeps each plugin's config and state in its own directory, but it does not review or sandbox what a plugin does." On history: "pane output can include secrets, tokens, prompts, and command output."
trade-offThere is no isolation inside a session. Any agent in any pane gets the whole CLI, so it can read, prompt, or close every other pane. The skill file's guardrails ("do not close panes you did not create") are advisory prose, not an enforced rail.
linksserver/socket_paths.rs Β· docs/plugins.mdx Β· skills/herdr/SKILL.md
choseZero model dollars. herdr never calls an LLM. Everything it "understands" comes from priority-ordered TOML rules matched against the bottom of the terminal buffer, plus OSC title/progress escape sequences. The only spend is CPU on PTY reads and screen matching.
whyA runtime that has to be right about "is this agent stuck" cannot afford latency or hallucination. Deterministic matching also makes agent explain possible β you can see exactly which rule fired.
trade-offComprehension is capped at what someone wrote a rule for. Every new agent UI is a manifest edit, and the fallback when nothing matches is idle β the optimistic answer, not the safe one.
linksdetect/mod.rs Β· detect/manifests/claude.toml Β· detect/manifest_update.rs
choseThree concentric layers over one control surface: the agent skill file (for a coding agent), CLI wrappers (for scripts and humans), and the raw newline-delimited-JSON socket β 89 methods across server, workspace, worktree, tab, pane, layout, agent, events, integration and plugin namespaces.
why"Most automation should start with the CLI wrappers. Use the raw socket API only when you need direct request/response control or long-lived event subscriptions." The layers deliberately share a surface so a script and an agent are not learning two dialects.
trade-offThat surface is now a real dependency for other people's tools, so herdr carries a wire PROTOCOL_VERSION with a documented bump discipline and ships a machine-readable JSON Schema via herdr api schema. That is maintenance cost the "just a multiplexer" framing hides.
linksprotocol/wire.rs Β· api/mod.rs Β· cli/spec.rs Β· herdr-api.schema.json
choseThree units, deliberately unlike each other. Detection manifests (TOML rule files; bundled, remotely updatable, hot-reloadable, local override always wins). Integrations (herdr-authored hook or plugin assets that herdr writes into the agent's own config directory, each independently version-numbered). Plugins (a directory plus herdr-plugin.toml, any argv command in any language).
why"Plugins exist so Herdr can stay lean⦠There is no separate plugin SDK or restricted command set. The entire Herdr CLI is the plugin API."
trade-offThree units means three update clocks and three failure modes. Manifests self-heal in the background; integrations silently go stale until you run herdr integration status; plugins never update themselves at all.
linksdetect/manifest_update.rs Β· integration/registry.rs Β· plugin_command.rs
choseA first-class unknown state, a named fallback reason when nothing matched (default_known_agent_idle_fallback), and an agent explain command that prints the matched rule, the evidence flags, the manifest source and version, and why an update was skipped.
whyDocs: "unknown means an agent is present but Herdr cannot classify its lifecycle confidently; it does not prove successful completion." Contributor rules: "Screen detection is evidence-based. When changing manifests, first capture the relevant bottom-buffer state."
trade-offHonesty is pushed onto the caller. agent wait defaults to accepting idle, done, or blocked β so the default is "something settled," not "it worked." A caller who needs the distinction must spell out --until.
linksdetect/mod.rs Β· AGENTS.md Β· cli/agent.rs
choseFour ladders, strongest first: (1) live persistence β the client detaches, processes never stop; (2) snapshot restore β layout and cwd only; (3) pane screen history replay β opt-in, off by default; (4) native agent session resume β relaunch the agent with its own --resume <id>, on by default for 16 agents.
whyEach ladder solves a different failure. The docs open the page with a matrix of exactly what returns in each case rather than one marketing claim about "persistence."
trade-offThe rung you land on depends on facts you may not know at the time β whether the integration is installed, whether it is current, whether the session ref is stale. Anything unrecognised silently degrades to "a fresh shell in the right directory."
linkspersist/restore.rs Β· app/agent_resume.rs Β· agent_resume.rs Β· step 8 β
choseFive states β working, blocked, done, idle, unknown. The interesting one is done: it is the same underlying idle state as idle, distinguished only by whether a human has looked at it yet.
whyThe product problem is not "what is the agent doing," it is "what needs me." Promoting seen/unseen into the runtime's own state model is what makes the sidebar an inbox rather than a dashboard.
trade-offA presentation fact leaked into shared runtime state β which the repo's own runtime/client guardrail otherwise forbids. And it bites automation: reading a pane through the CLI deliberately does not mark it seen, so a script's view of done differs from the UI's.
linksdetect/mod.rs Β· workspace/aggregate.rs Β· step 5 β
choseExactly one authority per pane. If an agent has complete lifecycle hooks and its integration is installed and actively reporting, the hooks author idle/working/blocked and screen matching is switched off for that pane. Otherwise the screen manifest is authoritative. Six agents get hook authority; the rest get screen.
why"This avoids two competing sources of truth." Notably, integrations that only report session identity are deliberately excluded from state authority because "their hooks do not cover the whole lifecycle. They can miss permission approval results, escape interrupts, or other transitions."
trade-offA half-good hook is worse than none, so herdr has to maintain a hand-curated list of which integrations are trustworthy enough to be authoritative β a judgement call that has to be re-made every time an agent ships a new hook.
linkspane/agent_detection.rs Β· integration/registry.rs Β· step 4 β
choseState rolls up the tree β a blocked pane makes its tab and its workspace look blocked β and out to a notification with four delivery choices (in-app toast, outer-terminal escape sequence, OS notification, off), plus per-agent sound overrides.
whyThe stated core workflow: "start several agents, let them work in parallel, and use the sidebar to see which project needs a decision." The terminal delivery mode exists specifically so notifications survive an SSH session from a phone.
trade-offRollup is lossy by design β one blocked agent makes a whole workspace look blocked, so at high pane counts the sidebar tells you which project, not which pane.
linksworkspace/aggregate.rs Β· server/notifications.rs Β· config/sound.rs
choseThree separated primitives β layout, pane, agent β with a hard rule that agent start requires an already existing shell pane at its prompt and never creates, splits, or moves layout. Pane commands address the terminal whatever occupies it; agent commands resolve the live agent and refuse if it no longer owns the pane.
whySeparating "where a terminal is" from "what is in it" is what lets one agent safely puppet another: the caller composes layout explicitly instead of a convenience command guessing.
trade-offVerbosity. Spawning a helper is four calls and a jq β split, capture the id, start, prompt β and the skill file has to spend paragraphs on which id to read out of which JSON field.
linksskills/herdr/SKILL.md Β· cli/agent.rs Β· api/wait.rs Β· step 6 β
choseOpaque stable handles (w1, w1:t1, w1:p1), never reused after close. Agent names (reviewer) are aliases for the current occupant of a pane, cleared the moment that agent exits or is replaced. Waits pin the resolved occupant so a replacement process cannot satisfy someone else's wait.
whyIn a runtime where processes come and go under stable locations, a name that outlives its process is a correctness bug waiting to happen β you would wait on reviewer and be answered by whatever landed in that pane next.
trade-offMoving a pane between workspaces changes its public id, so callers must re-read it from .result.move_result.pane.pane_id, and an in-flight wait dies with agent_not_running.
linksapp/ids.rs Β· api/wait.rs Β· layout.rs
choseSplit the cost in two. Tuning an existing agent's rules is free and out-of-band: herdr fetches remote manifest updates from herdr.dev and hot-reloads them into the running server with no restart. Adding a brand new agent still needs a binary release, because process detection, labels, and integration behaviour are compiled in.
whyAgent CLIs change their UI weekly; herdr releases do not. Decoupling the fast-moving half from the release train is the only way the detection layer can keep up.
trade-offherdr's vendor now pushes rule updates to your machine by default ([update] manifest_check = false opts out). And third parties cannot add an agent β they can only report state inward via pane report-agent from a custom hook.
linksdetect/manifest_update.rs Β· server/autodetect.rs Β· step 10 β
choseAn experimental, opt-in live handoff: herdr update --handoff asks the old server to transfer live PTYs, agent identity, durable metadata, and plugin state to the replacement server, so pane processes survive the upgrade. Default herdr update uses the ordinary stop/restart.
whyA runtime whose whole promise is "your work survives" cannot make you kill that work to install a fix.
trade-offStated honestly in the docs: transient coordination does not cross the boundary β "In-flight CLI or API requests, waits, subscription streams, client sockets, and pane-to-pane messages may be interrupted; clients should reconnect and retry." It is also unavailable for Homebrew, mise, and Nix installs, whose updates herdr does not own.
linksserver/handoff.rs Β· handoff_runtime.rs Β· update.rs Β· step 9 β
Ten steps, following a single terminal from the moment the server takes it to the moment a new binary inherits it.
Typing herdr does not open an app β it starts (or finds) a background server and attaches a thin client to it. The server owns every PTY: the shell, the agent, the dev server, the test watcher. The client owns only pixels and keystrokes.
This split is the whole product. It is also an enforced architectural rule in the repo: new shared runtime facts must live in server state and be reachable over the JSON API, never only through the private TUI socket.
--session β HERDR_SOCKET_PATH β HERDR_SESSION β default.Panes sit in a three-level tree: workspace β tab β pane. One workspace per repo or investigation, tabs for views inside it, panes for actual terminals. Creating a workspace also creates its first tab and root pane in one call.
Git worktrees get first-class treatment: worktree create makes the checkout, opens it as a workspace, and groups it under the parent repo's row β so three branches of the same repo read as one project with three children.
w1, w1:t1, w1:p1) and never reuses them.A pane is just a terminal β it may hold a shell, a test runner, or a coding agent. herdr identifies the foreground process of each pane and maps it to a known agent kind. If a sandbox wrapper hides the real process, you can override with HERDR_AGENT=claude on the wrapper command.
In the other direction, herdr injects its own coordinates into every pane process β which is how an agent inside a pane knows it is inside herdr, and which pane it is.
HERDR_* environment every pane process inherits.blockedThis is the heart of herdr. Each pane has exactly one status authority. If the agent has complete lifecycle hooks and its integration is installed and reporting, those hooks author the state and screen matching is skipped entirely for that pane. Otherwise herdr reads the live bottom-of-buffer snapshot and runs a priority-ordered TOML rule ladder against it.
The snapshot is deliberately the live bottom of the buffer, not what you are looking at β scrolling back in herdr never confuses detection. And blocked is strict: if no rule matches, herdr falls back to idle and names that fallback in agent explain rather than guessing.
idle, the optimistic answer, so a brand-new approval dialog reads as ready rather than stuck.visible_blocker, visible_idle, skip_state_update) used for arbitration.contains/any/all/not gates.agent explain β prints matched rule, evidence, manifest source and version.A pane's state climbs the tree: a blocked agent makes its tab blocked and its workspace blocked, so the sidebar answers "which project needs me" without you opening anything. This rollup is the actual product β running eight agents is easy, noticing the one that stopped is not.
The done state is the twist. It is the same underlying idle as idle, held apart purely by whether you have looked at it. Focusing the tab marks it seen; reading it from the CLI deliberately does not.
Because every pane process inherits HERDR_ENV=1 and HERDR_BIN_PATH, the agent inside a pane can run the same herdr commands you do. That is the agent-native claim: an agent can split a pane, start a second agent, prompt it, and block until it settles β no SDK, no separate protocol.
Three primitives stay separate. Layout creates terminal locations. Pane commands drive a raw terminal whatever occupies it. Agent commands resolve the live agent and refuse if it has been replaced. agent start deliberately cannot create layout.
HERDR_ENV guard.herdr api schema).ctrl+b q kills the client. Nothing else. The server keeps every PTY running, and herdr reattaches to the same session later β from the same machine, from an SSH shell, or from a phone.
There are three attach shapes, and the difference is where the client runs. Running herdr after SSHing in puts everything on the server (simplest). herdr --remote host keeps the client local and streams the UI over SSH, which is what lets it bridge local desktop features like clipboard image paste into a remote session.
--remote thin client: SSH bootstrap, binary probing, keybinding snapshot.Reboot the machine and the strongest guarantee is gone β the processes died. herdr then climbs down four ladders, and the docs are unusually blunt about what each one actually returns.
The best of the weak paths is native agent session resume: if an official integration reported the agent's own session id, herdr relaunches that agent with its own --resume flag. Sixteen agents support it and it is on by default. Everything unrecognised comes back as a plain shell in the right directory.
session.json and the opt-in session-history.json.Updating a runtime whose promise is "your work survives" is awkward: normally you must stop the server, which stops everything. herdr update --handoff is the experimental answer β the old server passes its live PTYs and durable state to the new binary's server.
What crosses is explicit: pane processes, agent identity and durable metadata, plugin state. What does not cross is equally explicit: in-flight requests, waits, subscriptions, client sockets, pane-to-pane messages. Clients are expected to reconnect and retry.
PROTOCOL_VERSION and the compatibility rule that decides whether a restart is required.herdr keeps its core small on purpose and pushes everything else out to three unlike extension units. They differ most in who updates them and when β which is the thing to steal if you are designing your own extension model.
Detection manifests self-update from herdr.dev and hot-reload with no restart. Integrations are herdr-authored assets written into each agent's own config directory, each independently version-numbered, and they go stale silently. Plugins are yours entirely β any language, any argv command, the whole CLI as their API, and no sandbox.
Every module in src/ that carries a distinct job, grouped by what it does, chipped by which side of the server/client line it lives on. 236 Rust files total; this is the map, not the census.
| Module | Side | What it does |
|---|---|---|
| server/mod.rs | server | The background process that owns everything durable. |
| server/client_accept.rs | server | Accepts and authenticates attaching clients on the session socket. |
| server/client_transport.rs | server | Framing and transport for the client connection. |
| server/clients.rs | server | Tracks each attached client's size, theme, and focus. |
| server/socket_paths.rs | server | Session socket resolution order and named-session paths. |
| server/headless.rs | server | Running with no client attached β the normal state most of the time. |
| session.rs | server | Named session namespaces, data dirs, and lifecycle. |
| ipc.rs | shared | Local IPC primitives under the socket API. |
| pty/actor.rs | server | One actor per terminal pumping PTY bytes. |
| pty/backend.rs | server | Platform PTY allocation and process spawning. |
| platform/mod.rs | shared | Shared traits only; OS behaviour lives in the per-OS files beside it. |
| noninteractive_process.rs | server | Running helper commands without a terminal. |
| Module | Side | What it does |
|---|---|---|
| workspace/mod.rs | server | Top-level project container; owns tabs and git provenance. |
| workspace/tab.rs | server | A tab is one pane layout inside a workspace. |
| workspace/aggregate.rs | server | The rollup: pane state β tab state β workspace state. |
| workspace/git/status.rs | server | Branch and dirty status shown on workspace rows. |
| layout.rs | server | Split tree, ratios, neighbours, zoom, export/apply. |
| pane.rs | server | Pane record: its terminal, label, and metadata. |
| pane/state.rs | server | Per-pane durable state, separate from its runtime. |
| worktree.rs | server | Git worktree checkouts opened as grouped workspaces. |
| app/state.rs | server | Pure data AppState, testable with no PTYs and no async. |
| app/ids.rs | server | Opaque public ids, never reused after close. |
| app/runtime.rs | server | The live half that state deliberately does not know about. |
| app/creation.rs | server | Creating a workspace also creates its first tab and root pane. |
| Module | Side | What it does |
|---|---|---|
| detect/mod.rs | server | State enum, evidence flags, and the agent kind list. |
| detect/manifest.rs | server | The TOML rule engine: regions, priorities, and gate combinators. |
| detect/manifest_update.rs | server | Remote manifest fetch, validation, and hot reload. |
| detect/manifests/ | server | 19 bundled per-agent rule files. |
| server/autodetect.rs | server | The always-on sampling loop over every pane. |
| pane/agent_detection.rs | server | Per-pane authority arbitration between hooks and screen. |
| pane/osc.rs | server | OSC title and progress sequences as detection evidence. |
| app/agents.rs | server | Agent registry, names, uniqueness, release on exit. |
| app/agent_view.rs | server | Server-side agent view queries used by clients and the API. |
| metadata_tokens.rs | server | Display-only tokens and labels, isolated from semantic state. |
| server/alt_screen_read.rs | server | Scrolls an idle agent's alternate screen to recover transcript history. |
| app/terminal_titles.rs | server | Safety-normalised terminal titles, ephemeral across cold restart. |
| Module | Side | What it does |
|---|---|---|
| cli/spec.rs | shared | The full command tree: 17 top-level groups plus completions. |
| cli/agent.rs | shared | agent start Β· prompt Β· wait Β· read Β· explain Β· rename Β· attach. |
| cli/pane.rs | shared | pane split Β· run Β· send-keys Β· read Β· wait-output Β· report-agent. |
| cli/workspace.rs | shared | Workspace create, list, focus, move, close. |
| cli/worktree.rs | shared | Worktree create, open, remove β the explicit deletion path. |
| cli/plugin.rs | shared | Plugin link, install, enable, invoke, logs. |
| cli/integration.rs | shared | Integration install, uninstall, status. |
| cli/protocol_guard.rs | shared | Refuses to talk to an incompatible server version. |
| api/mod.rs | server | The 89-method request router. |
| api/wait.rs | server | Event-driven waits that pin the resolved occupant. |
| api/event_hub.rs | server | Event fan-out to subscribers and waiters. |
| api/subscriptions.rs | server | Long-lived subscription streams over one connection. |
| api/schema.rs | shared | Machine-readable JSON Schema for the whole protocol. |
| protocol/wire.rs | shared | PROTOCOL_VERSION and the wire format. |
| protocol/render_ansi.rs | shared | Terminal state β ANSI frames for attach and observe. |
| Module | Side | What it does |
|---|---|---|
| persist/snapshot.rs | server | Serialises session shape. |
| persist/restore.rs | server | Chooses each pane's restore ladder on boot. |
| persist/io.rs | server | session.json and opt-in session-history.json. |
| persist/plugin_registry.rs | server | Which plugins are linked and enabled, across restarts. |
| agent_resume.rs | server | Session reference β the agent's own resume command. |
| app/agent_resume.rs | server | Drives resume across all restored panes after a client attaches. |
| server/handoff.rs | server | Live PTY transfer to a replacement server. |
| handoff_runtime.rs | server | The state shape that crosses the handoff boundary. |
| remote/attach.rs | client | SSH thin-client attach, bootstrap, and binary probing. |
| remote/host_unix.rs | server | The remote-host side of that bootstrap. |
| server/terminal_attach.rs | server | Direct single-terminal attach, takeover, and read-only observe. |
| update.rs | shared | Self-update and release channels (stable / preview). |
| Module | Side | What it does |
|---|---|---|
| integration/registry.rs | server | Per-agent integration table and versions. |
| integration/targets.rs | server | Resolves each agent's own config directory. |
| integration/config_edit.rs | server | Surgical edits that leave other people's config entries alone. |
| integration/env.rs | server | The HERDR_* environment injected into every pane. |
| plugin_command.rs | server | Launches plugin commands with injected runtime context. |
| plugin_paths.rs | server | Per-plugin config, state, and log locations. |
| server/notifications.rs | server | When a state change earns an interruption. |
| terminal_notify.rs | client | Outer-terminal notifications that survive SSH. |
| sound.rs | client | Local sound with per-agent overrides. |
| ui.rs | client | The mouse-first TUI shell. |
| config/model.rs | shared | The TOML config schema β the one file you own. |
| config/keybinds.rs | client | Prefix keys, navigate mode, custom command bindings. |
| ghostty/ | server | Bindings to the vendored libghostty-vt terminal parser. |
| kitty_graphics.rs | shared | Image protocol support inside panes. |
| Group | What it is for |
|---|---|
| herdr | Attach or start the session UI. --remote, --session, --handoff, --no-session, --skill. |
| workspace Β· tab Β· pane | Layout: create, list, focus, split, swap, resize, move, close. |
| agent | start Β· prompt Β· wait Β· read Β· explain Β· send-keys Β· rename Β· focus Β· attach. |
| worktree | Git checkouts as grouped workspaces; remove is the explicit delete. |
| terminal | Direct attach, read-only observe, writable control of one terminal. |
| session | Named runtime namespaces: list, attach, stop, delete. |
| server | stop Β· reload-config Β· reload-agent-manifests Β· update-agent-manifests. |
| integration | install Β· uninstall Β· status for the 16 official agent integrations. |
| plugin | link Β· install Β· list Β· enable Β· disable Β· action invoke Β· logs. |
| api Β· config Β· status | Print the protocol schema, the default config, and server health. |
| channel Β· update | Switch stable/preview; self-update, optionally with live handoff. |
| notification Β· completion | Fire a notification from a script; emit shell completions. |