AI-dev atlas Β· agent runtime

herdr

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.

server lives in the background server β€” survives detach client lives in the attached terminal UI β€” dies on detach shared spans both

What it is

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.

The glossary map

Shared termherdr calls itWhere it lives
body vs brainruntime / "the runtime your coding agents live on"herdr is pure body β€” it has no memory layer at all
dispatcherstatus authoritydetect/mod.rs β€” picks which signal may author state
filing gatemanifest rule / priority ladderdetect/manifest.rs
always-onautodetect loopserver/autodetect.rs
skillagent skill fileskills/herdr/SKILL.md
gate vs advisoryproposed term β€” herdr's version is state authority: exactly one signal per pane may say blockedpane/agent_detection.rs
artifact handofflive handoff β€” but of PTYs, not filesserver/handoff.rs

Body vs brain

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.


Key decisions

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.

C1Source of truth

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

C2Trust & safety boundaries

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

C3Cost model

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

C4Integration surface

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

C5Extension model

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

C6Honesty discipline draft question

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

R1Durability boundary draft bank

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 ↓

R2Agent state model draft bank

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 ↓

R3State authority & conflict resolution draft bank

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 ↓

R4Attention routing draft bank

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

R5Agent self-orchestration draft bank

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 ↓

R6Identity & addressing draft bank

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

R7Cost of supporting a new agent draft bank

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 ↓

R8Self-replacement draft bank

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 ↓


The life of one pane

Ten steps, following a single terminal from the moment the server takes it to the moment a new binary inherits it.

01

The server takes the terminal

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.

your terminal TUI client renders Β· sends keys mouse Β· theme Β· size dies on detach input frames unix socket / named pipe herdr server β€” keeps running pane w1:p1PTY β†’ claude pane w1:p2PTY β†’ codex pane w1:p3PTY β†’ tests session state Β· agent identity Β· metadata Β· plugin state Β· event hub persisted shape β†’ session.json ctrl+b q detaches this box only
Everything green survives the lid closing; everything amber is rebuilt on reattach. The socket is the only bridge, and it is local-only β€” remote access is SSH carrying that same client.
  • server/mod.rs serverThe background process that owns panes, terminals, agents, and the event hub.
  • server/client_accept.rs serverAccepts attaching clients on the session socket; multiple clients can watch one session.
  • server/socket_paths.rs serverResolves which socket to use: --session β†’ HERDR_SOCKET_PATH β†’ HERDR_SESSION β†’ default.
  • pty/actor.rs serverOne actor per terminal, pumping bytes between the process and the parser.
  • client/mod.rs clientThe attached TUI: draws frames, captures mouse and keys, owns nothing durable.
  • server/render_stream.rs sharedStreams rendered terminal frames to each attached client.
02

Where the pane lives

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.

workspace w1 ~/project Β· one repo tab w1:t1agents tab w1:t2logs w1:p1claude w1:p2shell w1:p3tail -f herdr worktree create --branch fix/x grouped under the parent row workspace w1 (parent) closing this closes the whole group w2 Β· fix/x β†’ ~/.herdr/worktrees/repo/fix-x its own tabs, panes, agents w3 Β· spike/y workspace close β‰  checkout delete deleting a checkout is a separate, confirmed action
The tree is server-owned, so it survives detach. The worktree branch is the one place herdr touches your repo β€” and it separates "close the workspace" from "delete the checkout" so the destructive half is always explicit.
  • workspace/mod.rs serverWorkspace container: owns tabs, git provenance, and grouping.
  • workspace/tab.rs serverA tab is one pane layout inside a workspace.
  • layout.rs serverSplit tree, ratios, neighbours, zoom β€” the geometry of panes.
  • worktree.rs serverCreates, opens, and removes git worktree checkouts behind grouped workspaces.
  • workspace/git/discovery.rs serverFinds the repo and branch behind a workspace's cwd.
  • app/ids.rs serverMints the opaque public ids (w1, w1:t1, w1:p1) and never reuses them.
03

Working out what is running in there

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.

pane PTY foreground pgid / child groups process β†’ kind 21 known executables HERDR_AGENT=claude escape hatch for sandbox wrappers agent identity kind Β· label Β· name session ref (if any) feeds steps 04–05 injected into the pane HERDR_ENV=1 HERDR_PANE_ID HERDR_TAB_ID Β· HERDR_WORKSPACE_ID HERDR_BIN_PATH HERDR_SOCKET_PATH the pane process can now call back in β€” see step 06
Identity is decided from the process, not the screen. That ordering matters: the screen rules in step 4 are only ever evaluated against the manifest for the agent already identified here.
  • server/autodetect.rs serverThe always-on loop that samples each pane's foreground process and screen.
  • platform/unix_common.rs serverForeground process-group lookup; the OS-specific half of detection.
  • pane/agent_detection.rs serverHolds per-pane agent identity and arbitrates which source may author state.
  • integration/env.rs serverBuilds the HERDR_* environment every pane process inherits.
  • app/agents.rs serverAgent registry: names, aliases, uniqueness, release on exit.
  • pane/osc.rs serverParses OSC title and progress sequences used as extra detection evidence.
04

Who gets to say blocked

This 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.

pane, agent known from step 03 complete lifecycle hooks installed AND reporting? yes Β· 6 agents no Β· everyone else hook authority pane.report_agent source Β· state Β· seq pi Β· omp Β· kimi Β· opencode kilo Β· mastracode screen matching switched OFF screen manifest ladder live bottom-buffer snapshot, not the viewport 1100 osc_title_working β†’ working 1000 transcript_viewer β†’ skip update 980 live_blocked_form β†’ blocked 950 live_prompt_box β†’ idle 850 bash_permission β†’ blocked …first match by priority wins one state for this pane no rule matched β†’ idle, reason named
The fork is exclusive on purpose β€” "this avoids two competing sources of truth." Note the asymmetry the docs are honest about: nothing-matched resolves to idle, the optimistic answer, so a brand-new approval dialog reads as ready rather than stuck.
  • detect/mod.rs serverThe state enum plus the evidence flags (visible_blocker, visible_idle, skip_state_update) used for arbitration.
  • detect/manifest.rs serverThe rule engine: regions, priorities, contains/any/all/not gates.
  • detect/manifests/*.toml server19 bundled rule files, one per recognised agent UI.
  • server/alt_screen_read.rs serverDrives an idle agent's own scrollback to read transcript history off the alternate screen.
  • api/status.rs serverExposes state and the explain payload over the API.
  • cli/agent.rs sharedagent explain β€” prints matched rule, evidence, manifest source and version.
05

Rolling up, and ringing the bell

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.

p1 working p2 blocked p3 idle tab w1:t1 blocked workspace w1 blocked the sidebar row that catches your eye worst-state-wins rollup notify herdr Β· in-app toast terminal Β· works over SSH system Β· OS notifier off + per-agent sounds suppressed for the active tab the seen flag done idle focus the tab β†’ seen pane focus / agent focus β†’ seen CLI read β†’ NOT seen same idle underneath, different inbox meaning
The rollup makes a workspace list behave like an inbox. The cost is precision β€” one blocked pane paints an entire workspace, so the sidebar tells you the project, and you still open it to find the pane.
  • workspace/aggregate.rs serverRolls pane states up into tab and workspace states, carrying the seen flag.
  • server/notifications.rs serverDecides when a state change deserves a notification, and suppresses the active tab.
  • terminal_notify.rs clientOuter-terminal notification escape sequences β€” the delivery mode that survives SSH.
  • sound.rs clientPlays finished/needs-input sounds locally, with per-agent overrides.
  • metadata_tokens.rs serverDisplay-only tokens and state labels, kept strictly apart from semantic state.
  • config/sidebar.rs clientSidebar row layouts β€” which tokens and titles appear where.
06

The agent drives back

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.

agent skill file teaches an agent the CLI CLI wrappers 17 command groups raw socket 89 methods Β· ndjson one surface server control surface layout workspace Β· tab Β· split β€” makes locations pane run Β· send-keys Β· read Β· wait-output agent start Β· prompt Β· wait Β· explain β€” needs a shell pane pane w1:p1 β€” the caller HERDR_ENV=1 Β· HERDR_PANE_ID=w1:p1 runs $HERDR_BIN_PATH like any shell skill guard: no HERDR_ENV β†’ stop pane w1:p2 β€” the helper split --no-focus β†’ agent start reviewer agent prompt reviewer "…" --wait wait pins this occupant calls out results and state come back the guard that makes this safe no HERDR_ENV=1 β†’ the agent must refuse, so an agent outside herdr can never drive a session it does not own
One surface, three doors. The separation of pane from agent is what makes puppeting safe: pane input goes to the terminal regardless of occupant, agent input is rejected if the agent it named no longer controls that pane.
  • skills/herdr/SKILL.md sharedThe instruction file installed into a coding agent; opens with the HERDR_ENV guard.
  • cli/spec.rs sharedThe whole command tree β€” 17 groups, also the source of shell completions.
  • api/wait.rs serverServer-owned, event-driven waits that pin the resolved pane occupant.
  • api/event_hub.rs serverFan-out of runtime events to subscribers and waiters.
  • api/subscriptions.rs serverLong-lived event subscriptions over one socket connection.
  • api/schema.rs sharedEmits the JSON Schema for requests, responses, and events (herdr api schema).
07

The lid closes

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.

the one running server PTYs never stopped Β· state unchanged claude Β· working codex Β· blocked multiple clients may attach at once one writable direct-attach owner per terminal herdr local client, local server ssh host β†’ herdr client + server both remote Β· phone-friendly herdr --remote host local thin client Β· bridges clipboard images agent attach / terminal observe one terminal, no workspace UI all four land on the same session ssh
Detach is not a save operation β€” there is nothing to save, because nothing stopped. The three shapes differ only in which side of the SSH connection the rendering client sits on.
08

When the server does stop

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.

strongest weakest 1 Β· live persistence β€” client detached, server never stopped processes βœ“   layout βœ“   screen βœ“ (it is the live terminal)   conversation βœ“ (it never ended) 2 Β· snapshot restore β€” always on, session.json processes βœ—   layout βœ“   cwd βœ“   focus βœ“   screen βœ—   conversation βœ— β†’ panes return as fresh shells 3 Β· pane screen history replay β€” OFF by default, [experimental] pane_history = true recent screen text βœ“   processes βœ—   off because output can contain secrets, tokens, and prompts 4 Β· native agent session resume β€” ON by default, 16 agents relaunches the agent with its own flag: claude --resume <id> Β· codex resume <id> Β· opencode --session <id> … nothing to rebuild shape only opt-in, secret-bearing stale ref β†’ plain shell
Ladder 4 beats ladder 3 when both apply β€” if a pane can resume its native agent session, herdr does that instead of replaying saved screen text. The honest read: only ladder 1 preserves your work; 2–4 preserve increasingly thin shadows of it.
  • persist/snapshot.rs serverSerialises the session shape β€” workspaces, tabs, panes, cwd, layout, focus.
  • persist/restore.rs serverRebuilds that shape on boot and decides each pane's restore path.
  • persist/io.rs serverWhere it lands: session.json and the opt-in session-history.json.
  • agent_resume.rs serverMaps a stored native session reference to the agent's own resume command line.
  • app/agent_resume.rs serverDrives resume across all restored panes once a client supplies size and theme.
  • persist/plugin_registry.rs serverPersists which plugins are linked and enabled across restarts.
09

Replacing the runtime without killing the work

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.

old server Β· v0.x live PTYs + processes agent identity + metadata plugin / session state in-flight requests Β· waits Β· subscriptions transferred β€” processes keep running dropped β€” clients reconnect and retry new server Β· v0.y same panes, same agents, same work no shell restarted, no conversation lost opt-in: herdr update --handoff herdr --remote host --handoff unavailable on brew / mise / nix installs plain `herdr update` uses the ordinary stop β†’ restart β†’ snapshot-restore path (step 08, ladder 2)
Handoff moves the durable half and openly abandons the transient half. It is the only mechanism here that keeps the strongest restore ladder intact through a version change β€” which is why it is opt-in and still marked experimental.
  • server/handoff.rs serverNegotiates the transfer and hands live terminals to the replacement server.
  • handoff_runtime.rs serverThe runtime state that must survive the swap, and the shape it travels in.
  • update.rs sharedThe self-updater; also the reason handoff is unavailable for package-manager installs.
  • protocol/wire.rs sharedPROTOCOL_VERSION and the compatibility rule that decides whether a restart is required.
  • cli/protocol_guard.rs sharedStops a mismatched CLI from talking to an incompatible server.
  • pty/fd.rs serverThe file descriptors that actually get passed across the boundary.
10

Three extension surfaces, three clocks

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.

detection manifests 19 TOML rule files ~/.config/herdr/agent-detection/ β€” wins cached remote from herdr.dev bundled in the binary clock: automatic background fetch β†’ in-memory reload no restart Β· opt out with manifest_check=false integrations 16 agents Β· hooks and plugins written INTO the agent's own config dir reports state and/or session identity in 6 are lifecycle authorities Β· 10 session-only clock: manual, versioned herdr integration status β†’ reinstall a stale version silently loses resume support plugins herdr-plugin.toml + any argv command actions Β· startup hooks Β· event hooks panes Β· keybindings Β· link handlers the entire herdr CLI is the plugin API clock: never β€” you own it min_herdr_version is the only guard no sandbox Β· runs as you Β· vet before install the fast-moving surface (agent UIs) is the one herdr decoupled from its release train
The design move worth copying: match the update clock to how fast the thing it tracks actually changes. Agent UIs change weekly, so manifests bypass releases entirely; plugin semantics change rarely, so plugins never auto-update.

Full roster

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.

The runtime spine

ModuleSideWhat it does
server/mod.rsserverThe background process that owns everything durable.
server/client_accept.rsserverAccepts and authenticates attaching clients on the session socket.
server/client_transport.rsserverFraming and transport for the client connection.
server/clients.rsserverTracks each attached client's size, theme, and focus.
server/socket_paths.rsserverSession socket resolution order and named-session paths.
server/headless.rsserverRunning with no client attached β€” the normal state most of the time.
session.rsserverNamed session namespaces, data dirs, and lifecycle.
ipc.rssharedLocal IPC primitives under the socket API.
pty/actor.rsserverOne actor per terminal pumping PTY bytes.
pty/backend.rsserverPlatform PTY allocation and process spawning.
platform/mod.rssharedShared traits only; OS behaviour lives in the per-OS files beside it.
noninteractive_process.rsserverRunning helper commands without a terminal.

Layout & session shape

ModuleSideWhat it does
workspace/mod.rsserverTop-level project container; owns tabs and git provenance.
workspace/tab.rsserverA tab is one pane layout inside a workspace.
workspace/aggregate.rsserverThe rollup: pane state β†’ tab state β†’ workspace state.
workspace/git/status.rsserverBranch and dirty status shown on workspace rows.
layout.rsserverSplit tree, ratios, neighbours, zoom, export/apply.
pane.rsserverPane record: its terminal, label, and metadata.
pane/state.rsserverPer-pane durable state, separate from its runtime.
worktree.rsserverGit worktree checkouts opened as grouped workspaces.
app/state.rsserverPure data AppState, testable with no PTYs and no async.
app/ids.rsserverOpaque public ids, never reused after close.
app/runtime.rsserverThe live half that state deliberately does not know about.
app/creation.rsserverCreating a workspace also creates its first tab and root pane.

Agent detection & state

ModuleSideWhat it does
detect/mod.rsserverState enum, evidence flags, and the agent kind list.
detect/manifest.rsserverThe TOML rule engine: regions, priorities, and gate combinators.
detect/manifest_update.rsserverRemote manifest fetch, validation, and hot reload.
detect/manifests/server19 bundled per-agent rule files.
server/autodetect.rsserverThe always-on sampling loop over every pane.
pane/agent_detection.rsserverPer-pane authority arbitration between hooks and screen.
pane/osc.rsserverOSC title and progress sequences as detection evidence.
app/agents.rsserverAgent registry, names, uniqueness, release on exit.
app/agent_view.rsserverServer-side agent view queries used by clients and the API.
metadata_tokens.rsserverDisplay-only tokens and labels, isolated from semantic state.
server/alt_screen_read.rsserverScrolls an idle agent's alternate screen to recover transcript history.
app/terminal_titles.rsserverSafety-normalised terminal titles, ephemeral across cold restart.

Control surface β€” CLI, API, protocol

ModuleSideWhat it does
cli/spec.rssharedThe full command tree: 17 top-level groups plus completions.
cli/agent.rssharedagent start Β· prompt Β· wait Β· read Β· explain Β· rename Β· attach.
cli/pane.rssharedpane split Β· run Β· send-keys Β· read Β· wait-output Β· report-agent.
cli/workspace.rssharedWorkspace create, list, focus, move, close.
cli/worktree.rssharedWorktree create, open, remove β€” the explicit deletion path.
cli/plugin.rssharedPlugin link, install, enable, invoke, logs.
cli/integration.rssharedIntegration install, uninstall, status.
cli/protocol_guard.rssharedRefuses to talk to an incompatible server version.
api/mod.rsserverThe 89-method request router.
api/wait.rsserverEvent-driven waits that pin the resolved occupant.
api/event_hub.rsserverEvent fan-out to subscribers and waiters.
api/subscriptions.rsserverLong-lived subscription streams over one connection.
api/schema.rssharedMachine-readable JSON Schema for the whole protocol.
protocol/wire.rssharedPROTOCOL_VERSION and the wire format.
protocol/render_ansi.rssharedTerminal state β†’ ANSI frames for attach and observe.

Persistence, remote, and continuity

ModuleSideWhat it does
persist/snapshot.rsserverSerialises session shape.
persist/restore.rsserverChooses each pane's restore ladder on boot.
persist/io.rsserversession.json and opt-in session-history.json.
persist/plugin_registry.rsserverWhich plugins are linked and enabled, across restarts.
agent_resume.rsserverSession reference β†’ the agent's own resume command.
app/agent_resume.rsserverDrives resume across all restored panes after a client attaches.
server/handoff.rsserverLive PTY transfer to a replacement server.
handoff_runtime.rsserverThe state shape that crosses the handoff boundary.
remote/attach.rsclientSSH thin-client attach, bootstrap, and binary probing.
remote/host_unix.rsserverThe remote-host side of that bootstrap.
server/terminal_attach.rsserverDirect single-terminal attach, takeover, and read-only observe.
update.rssharedSelf-update and release channels (stable / preview).

Extension & presentation

ModuleSideWhat it does
integration/registry.rsserverPer-agent integration table and versions.
integration/targets.rsserverResolves each agent's own config directory.
integration/config_edit.rsserverSurgical edits that leave other people's config entries alone.
integration/env.rsserverThe HERDR_* environment injected into every pane.
plugin_command.rsserverLaunches plugin commands with injected runtime context.
plugin_paths.rsserverPer-plugin config, state, and log locations.
server/notifications.rsserverWhen a state change earns an interruption.
terminal_notify.rsclientOuter-terminal notifications that survive SSH.
sound.rsclientLocal sound with per-agent overrides.
ui.rsclientThe mouse-first TUI shell.
config/model.rssharedThe TOML config schema β€” the one file you own.
config/keybinds.rsclientPrefix keys, navigate mode, custom command bindings.
ghostty/serverBindings to the vendored libghostty-vt terminal parser.
kitty_graphics.rssharedImage protocol support inside panes.

The CLI surface, at a glance

GroupWhat it is for
herdrAttach or start the session UI. --remote, --session, --handoff, --no-session, --skill.
workspace Β· tab Β· paneLayout: create, list, focus, split, swap, resize, move, close.
agentstart Β· prompt Β· wait Β· read Β· explain Β· send-keys Β· rename Β· focus Β· attach.
worktreeGit checkouts as grouped workspaces; remove is the explicit delete.
terminalDirect attach, read-only observe, writable control of one terminal.
sessionNamed runtime namespaces: list, attach, stop, delete.
serverstop Β· reload-config Β· reload-agent-manifests Β· update-agent-manifests.
integrationinstall Β· uninstall Β· status for the 16 official agent integrations.
pluginlink Β· install Β· list Β· enable Β· disable Β· action invoke Β· logs.
api Β· config Β· statusPrint the protocol schema, the default config, and server health.
channel Β· updateSwitch stable/preview; self-update, optionally with live handoff.
notification Β· completionFire a notification from a script; emit shell completions.
← Accession