Sidebar net view
The optional Herdr plugin in herdr-plugin/ (id wayne930242.straw-boss, action net) toggles the sidebar’s Agents list between Herdr’s own view and a net view. The net view lists the head tower’s workspace first when it exists (its row is the crown tab’s pane, and focusing it shows the standing pane, where Enter talks to the head tower), then the other workspaces in Herdr’s order. In each workspace the tower comes first, then the tabs in their order, and in each tab the main agent and then the members. Every subagent sits directly under the session that launched it, including one that pi-herdr-agents opened in a worktree sub workspace.
Agents the net does not know come last, in Herdr’s workspace, tab and pane order, so nothing is hidden and an agent that waits for you stays on the list. The sidebar header reads net while the view is on. The view is on by default: the plugin turns it on when the Herdr server starts, and the key switches it off and on again. The same order drives the collapsed sidebar, the mobile list, mouse targets, and focus_agent numbering and next/previous navigation. The view changes no notification or attention count.
The tower between runs
Section titled “The tower between runs”A tower the sb server runs keeps its row when no run is live: the server reports a stand-in agent tower on the standing pane (idle, with standing by, 3 queued or held as its message) while the tower prompt runs there (The standing pane), so the row sits first in its workspace as the tower’s mark and agent.list shows it. It is a row and nothing more: no net, dashboard, stash, closer or reaper check counts it as an agent, a main, or a member, and an idle report starts no toast, sound, or attention count. A run replaces it with pi’s own row, and it comes back when the run ends. An ally run’s pane is a row too, right under the tower ([ally] <device label>, see Ally runs).
Details
The tokens. Every session in the net writes display tokens next to sb_role, in the same report, on the same 4 minute refresh and with the same 10 minute TTL. It writes them on the panes of the subagents it launched as well, because those never join and write nothing themselves. A session that ends clears its own and its subagents’.
-
sb_orderis the sort key. A tower’s isWWW.0. A main agent’s or member’s isWWW.1.TTT.R.PPP:WWWis the place of its workspace among the base workspaces (a sub workspace does not count, and the head tower’s workspace is000while it exists, so the base workspaces then start at001),TTTthe place of its tab in the workspace,Ris0for a main agent and1for a member, andPPPthe place of its pane in the tab.Two more places sit between the tower and the tabs, for the merge train (see Merge train): the integrator, the main whose
traintodos made the project’syard.json(the yard names its session as writer, and that writer’s process still runs), isWWW.05.TTT.0.PPPand the members of its tabWWW.05.TTT.1.PPP, with the place of its tab among the workspace’s tabs asTTT, so its tab moves up under the tower and two integrators in one workspace are two blocks, each member right under its own main; a train isWWW.06.YYMMDDLL.HHHHHHHH, the year, month and day of its code and its letters left-padded with0(2026-11-06-mis2611060m,2026-11-06-aais261106aa), then the first eight hex digits of the SHA-256 of its project’s name, which only breaks the tie between trains of two projects that have the same code.The tower’s subagents (
WWW.0.NNN) come first because.sorts before digits, thenWWW.05, thenWWW.06, then the tabs (WWW.1). A train’s key depends on its own code and project alone, so a train that ends moves no other row and only thefocus_agentnumbers after it shift; one-letter codes sort before two-letter ones. A train’s reviewer is one segment below its train, as any subagent. Each subagent adds one.NNNsegment, its place among the live and finished subagents of its launcher, at any depth (the dashboard lists eight per session, the keys cover every live one).Every number is zero-padded, so plain string order is numeric order, and a subagent’s key sorts after its launcher’s and before the launcher’s next sibling’s.
-
sb_marksays what a net member is, in a few characters: the glyph of its kind alone for a main agent (since 0.74.0: a ship’s wheel, nf-md-ship_wheel U+F0833, for a coordinator, the code-fork below for an integrator, a lighthouse, nf-md-lighthouse U+F09FF, for a standing main; a star alone, Font Awesome’s U+F005, when the main’s kind is unknown), a person alone (Font Awesome’s, U+F007) for a member, and for a subagent the glyph of the kind its launch script names (PI_SUBAGENT_AGENT) alone: a wrench (nf-md-wrench U+F05B7) for aworker, binoculars (nf-md-binoculars U+F00A5) for ascout, a pencil and ruler (nf-md-pencil_ruler U+F1353) for aplanner, a magnifier (nf-md-magnify U+F0349) for areviewer, and a dog (nf-md-dog U+F0A43) for awatchdog.A subagent whose role is none of those, or whose launch script is gone, is
[sub] <launcher>, with the launcher’s name cut to 24 characters. The glyphs are the ones the page draws as icons (Browser view) and were checked against the glyph names of Nerd Fonts 3.5.1. The pane of an ally run (a Straw Link message worked by the sb server) has[ally] <device label>; its key, branch and name are written by the sb server for it, like a subagent’s by its launcher (see Ally runs). A tower has none: its tab already carries the radio tower glyph. -
sb_markalso marks the integrator with Font Awesome’s code-fork (U+F126) alone, and a train with its train glyph (U+F238) and the short code (\uF238 1106m). Both come from the trainboard files by the session’s own record, not from any token on a pane: the integrator is the session a liveyard.jsonnames (a yard whose writer process is gone names no one), the train is the one whose pane the tower opened for the code.A member of the integrator’s tab asks the intercom’s list where the sessions the live yards name sit, and takes the integrator’s place when one of them is in its own tab (Herdr says which tab a pane is in); it re-keys at its next refresh after the yard changes. The mark and the place are display only: a train’s own shell can write a yard that names a live session.
-
sb_stepis on a train alone: the glyph for its current step’s state (hourglass U+F254 waiting, play U+F04B running, check U+F00C passed, times U+F00D failed, undo U+F0E2 rolling back) and the step’s kind, at most 24 characters. The current step is the lowest one in progress, else the newest that completed, read from the train’s own board (validated, so the kind is one of the known words). The train writes it 5 seconds after its board changes (a watch on the project’s trainboard folder, which also looks once after it starts), besides the 4 minute refresh; control and direction characters are stripped. Every other agent carries none, and a session clears the one a pane kept. -
sb_main_tabis a main agent’s tab label andsb_taba member’s, one token per role because a Herdr rule sees only its own token, so a row colors each by role. A member’s keeps thesb_tabname it has had since 0.7.2, so the row works with sessions that have not reloaded; every other agent carries neither. -
sb_indentis on a subagent alone, and is the branch to it from its launcher, drawn like the worktrees in Herdr’s Spaces panel:├─while a later subagent of the same launcher is marked,└─on the last one. Subagents of one launcher are siblings at every depth. A tower, a main agent and a member carry none, and a session clears the one a pane kept from when it ran a subagent. -
sb_nameis on a subagent alone, and is its own name as pi-herdr-agents shows it, cut to 24 characters like the launcher’s name insb_mark. The name comes from the pane’s Herdr label while it has one, else from the launch script’s name. Herdr keeps the label in the server’s memory too, so after a new server the label is gone and a row would show only the workspace’s name; the token comes back with the others (see below) and a row can show it instead. A tower, a main agent and a member carry none, and a session clears the one a pane kept from when it ran a subagent.
The tokens rest on Herdr’s order as of the session’s last refresh, so moving a tab or workspace shows in the net view within 4 minutes, and a subagent that has a new sibling gets its ├─ at the next refresh. A subagent is marked while its pane runs pi: its launcher publishes again 3 and 15 seconds after a subagent or subagent_resume call returns, so a new subagent hangs under it within seconds, and its tokens go at the next refresh once it has finished. A subagent whose pi Herdr has not detected by then (it waits at pi’s trust prompt, say) is marked when Herdr reports it, not at the next refresh.
While a launched subagent has no pi pane, its launcher keeps one events.subscribe connection open for pane.agent_detected and pane.updated (the two events that can turn a pane into a pi on the subagent’s session file), and a burst of them, from a pane the launcher has not marked, sets one timer of 1 s that reads Herdr once. The reading writes only the subagents’ tokens, and only when they differ from the last ones written, so events that change nothing cost one reading and no write.
The connection closes as soon as every launched subagent has a pi pane, and with it a session that launched none holds none; a subagent that finished in the last 10 minutes has no pane either, so the connection stays that long after it. The 4 minute refresh stays as the safety net, and a subagent’s tokens still go at the next refresh once it has finished.
A new Herdr server
Section titled “A new Herdr server”Herdr keeps pane and workspace tokens in the server’s memory, so a server that takes over (a live handoff during an update, or a stop and a start) begins with none of them, and the net view would stay unsorted, and a tower would look absent, until each session’s next 4 minute refresh. Every session that can join the net, and every tower, therefore keeps one events.subscribe connection open on Herdr’s socket, only to see it close; it asks for workspace.renamed, which almost never happens, so the connection carries no traffic and costs nothing while the server runs.
Details
When it closes, the session reconnects after 0.25 s, 0.5 s, 1 s, and then every 2 s, and when a server answers, it writes everything again at once: first, for a tower, the workspace token tower and its own sb_role, which is how another session finds the tower (without them a call_tower in the gap would open a second one), then the full publish of the 4 minute refresh (its role and net tokens, and the marks of the subagents it launched). The first connection of a session is no takeover.
A session that has not finished joining (or a tower that has not taken its role) writes nothing on a takeover, and a subagent and a session in a sub workspace hold no connection.
A Herdr restore
Section titled “A Herdr restore”flowchart TD
r["herdr server stop,<br/>then a new server"] --> p["Herdr starts a new pi in each<br/>restored pane: pi --session file"]
p --> c{"Same pane, tab, and workspace<br/>as the recorded role?"}
c -->|"yes"| t["The session takes its role back"]
c -->|"no"| m["It stays a member"]
After herdr server stop and a new server, Herdr (with [session] resume_agents_on_restore, on by default) starts a new pi in every restored pane as pi --session <file>: the processes are new, the pane and tab ids and the session file path are the same, and STRAW_BOSS_ROLE, the tokens, and (for a session that had no conversation yet, since pi writes its file only after the first message, and gives the resumed one a new session id) the straw-boss entry are gone. Without more, every resumed session would come back a member: the PM who quits the app and opens it again would lose the tower and every main.
Details
A tower and a main therefore record their role in roles/<hash of the session file>.json of the state directory (role, pane, tab, workspace, and for a main the pane’s terminal), when the tower takes its role and when a session becomes its tab’s main, and drop it when the session is closed on purpose (the GUI’s quit, /boss close, a tower asking an opened main to quit, a second tower yielding).
A session that starts in the pane, tab, and workspace it recorded takes the role back: the tower starts as the tower once Herdr lists its pane in a tower tab (below), which writes its tokens and the workspace token with the new session id, and the main joins and promotes itself, refused as always when the tab already has a live main (and then it drops its record, since it stays a member).
A session in another pane, tab, or workspace than it recorded, one with no record or a record that cannot be read, and one a user closed on purpose stays a member, and nobody takes the role from a record without the pane, tab, and workspace it names. A main needs one more proof, that a restart happened: Herdr gives a restored pane a new terminal (terminal_id of pane current, while the pane id stays), and the record names the terminal the main ran in.
A pi started in the same terminal, after a /quit and a pi --session or /resume of the same file in the still-open shell, is no restore: it stays a member and drops the record, so the next real restore does not give the main role to a session that never held it. A tower and a train need another proof, since 0.75.0: a record and a saved entry only claim the role, and Herdr proves it. Both are files the session’s own shell can write, so a member that wrote {"role":"tower"} into its record, or a tower entry into its session file, used to start as a tower at its next /reload, /resume, or restore.
Now a session is a tower only when its process was started as one (STRAW_BOSS_ROLE=tower, or a tower earlier in the same process) or when Herdr lists its pane in a tab labelled with the tower glyph, which a restore keeps and no file holds (the proof the server seats its own towers by), and no other pane of that tab runs an agent; a train’s entry or record counts only in the tab labelled train <MMDDx> for its code, and at a /reload the entry must also equal the sb_train token its pane kept.
The label is asked with the workspace check at the start, so a tab list that fails proves nothing: the start stays unplaced and is asked again, and nothing joins meanwhile. A claim that is not proven starts a member (a train: no train) with one warning that names the file and the tab; so a tower’s session file resumed in an ordinary tab is a member, and a restored tower or train whose tab was renamed away from its label comes back a member.
boss_rename and a main’s label in boss_open_main refuse those labels, and /boss pop starts a tower only for a pane its manifest names a tower alone in a tower tab (anywhere else it comes back a member, with one warning). STRAW_BOSS_ROLE=tower itself is outside the guard: any shell can set it. A main has no such tab (its label is its own name), so its record stands on the checks above (spec §29).
A main a tower opened (see Closing an opened main) records its sb_opened_by marker too, and a restored main writes it back and registers the control channel again, so the restored tower of its workspace can close it as before: the restore is a plain start, which has neither the pane’s tokens nor the opener saved in the session, and the right to close rests on the tower token of the main’s workspace when the tower asks (the rule above), which a new tower session holds again once it is back.
The record counts only where the main ran (the same pane, tab, and workspace), so a main restored in another workspace has no marker, and a main the boss opened has none to bring back. The terminal is required for a main: a Herdr that reports no terminal_id for the pane gives it no restore, and the session warns once at its start. Herdr may reuse a pane id, so the record also names the session file, and the guard waits for a pane only while the agent_session Herdr reports for it (when it reports one) is that file.
A tab has one main record: the session that becomes the tab’s main deletes the others for that tab, so a main that /boss or the guard demoted does not come back as a second main whichever pi starts first; if an older record is left beside the new one (written by an earlier version), a restore honors only the newest. A record is not dropped when the guard turns a main back into a member, nor by a plain /quit: a Herdr stop looks the same from inside the process, and the tower quitting just ahead of its mains demotes them first. The guard that crowns a tab’s first member when the tab has a tower and no main (see the guard above) waits for the main a restore is bringing back: while the tab’s main record names a pane that now runs in a new terminal, it crowns nobody, for 30 s from its first look at that pane.
Without the wait the first pi to join a restored tab would be crowned over the recorded main, and the recorded main would then find the tab taken. A main that came back this way does not open a tower (the first main of a workspace normally does): its tower is coming back too, and a base whose tower does not return clears its mains after the guard’s grace as it always does. A record untouched for 90 days is deleted when another is written; that tidying, and the deletion of a rival main record, cannot fail the write of the record itself.
The connection and the retry timer never keep pi alive, and session_shutdown closes them: a /reload leaves one connection, the new instance’s, and a session started again in the same instance replaces its connection. A server that refuses events.subscribe is treated like one that is gone, so the session retries every 2 s without effect and keeps the 4 minute refresh as before. The sidebar plugin needs nothing more: its startup hook turns the view on when the server starts (before any token exists), and Herdr 0.9.4 and later sort the view again when tokens change.
The subagents a restore brings back are another matter: each comes back as a plain pi without the extension that makes it a child, and its launcher is asked to resume it (see Subagents a Herdr restore brought back plain).
Sidebar rows
Section titled “Sidebar rows”The tokens also show the roles in Herdr’s own grouped view, which a plugin cannot reorder. Add them to a row of [ui.sidebar.agents] in config.toml. $sb_indent goes before the state icon, so a subagent hangs under its launcher as the worktrees do in the Spaces panel; Herdr puts its own · after it, and an agent without the token shows nothing. $sb_name goes right after the state icon, so a subagent row says which subagent it is even when Herdr has lost the pane’s label, and $sb_step after it shows what a merge train is doing.
Details
Every glyph sits in the first row; the [sub] mark of a subagent with no kind stays hidden because the branch already says what it is, and a subagent’s kind glyph shows, since the branch does not. The snippet bolds the glyphs of the main kinds and leaves the colors to your Herdr theme; add an fg to a rule to color a mark. Herdr drops a row whose every item is empty, so the second row, the tab label from $sb_main_tab or $sb_tab, gives a main agent and a member a second line and leaves every other agent one line; give the two tokens the colors of the star and the person. The first row leaves out Herdr’s tab, which the second row carries.
[ui.sidebar.agents]rows = [ [ { token = "$sb_indent", dim = true }, "state_icon", { token = "$sb_name" }, { token = "$sb_step" }, { token = "$sb_mark", rules = [ { starts_with = "[sub]", hide = true }, { starts_with = "\uF005", bold = true }, { starts_with = "\uF007", bold = true }, { starts_with = "\uF126", bold = true }, { starts_with = "\uF238", bold = true }, { starts_with = "\U000F0833", bold = true }, { starts_with = "\U000F09FF", bold = true }, ] }, "machine", "workspace", ], [{ token = "$sb_main_tab", bold = true }, { token = "$sb_tab", bold = true }],]Herdr’s default has a second row with the agent name; put "agent" back in a row of its own, or in this one, if you want it. The token colours take hex values only, so add an fg to a token or a rule to taste. herdr config check accepts the snippet, and herdr server reload-config applies it.
Install. Link the plugin from a checkout and bind the action to a key. Herdr runs the startup hook when a server starts, not when a plugin is linked, so a server that is already running shows the view after one press of the key, after the next agent Herdr detects (the reapply event hook, below), or after a restart:
herdr plugin link /path/to/pi-straw-boss/herdr-pluginherdr plugin action list --plugin wayne930242.straw-bossherdr plugin action invoke wayne930242.straw-boss.net[[keys.command]]key = "prefix+alt+n"type = "plugin_action"command = "wayne930242.straw-boss.net"description = "toggle the straw net view"prefix+? lists the keys that are taken, herdr server reload-config loads a new binding, and herdr plugin unlink wayne930242.straw-boss removes the plugin. The plugin needs Herdr 0.9.5 or newer (the view probe is checked on it) and node; on Windows it reaches Herdr over its named pipe and has its own action, net-windows (Windows). The tokens come from the sessions, so a session that still runs an older pi-straw-boss (before 0.6.5) has none until pi reloads it, and sorts after the others.
How the view works
Section titled “How the view works”The view is one agent.view.set request on Herdr’s socket, which the CLI has no command for, sorted by the sb_order token ascending, then by Herdr’s workspace, tab and pane order. Herdr sorts a present value before a missing one, so agents without the token come after the net, and among themselves in that place order. The plugin writes no tokens of its own. Herdr servers before 0.9.4 do not sort an installed view again when tokens change (0.9.4 does): the order stays as it was at the last install until the view is switched off and on again, so press the key twice to refresh it.
Details
On at start. [[startup]] in the manifest runs net-view.mjs startup once after the server has restored its session, and again when a new server takes over in an update handoff. It installs the same view as the key and exits at once; no process stays running, and a failure (Herdr refusing the view) is in the plugin log and does not stop the server. Every server start turns the view on, whatever an earlier server left: a press that turned it off lasts until the server stops. The one exception is a startup that runs again in the same server, which leaves that server’s own “off” alone.
The toggle
Section titled “The toggle”Herdr has no query for the active view, but agent.view.clear with a source that owns nothing (<plugin source>-probe; Herdr accepts only ASCII letters, digits, colon, dot, underscore and hyphen in a source) leaves the view unchanged and answers with it (checked on Herdr 0.9.5), so the action reads what is shown instead of trusting what it saved: it clears when its own view is the active one and installs otherwise. A plugin unlinked and linked again, or disabled and enabled, in one server run loses its view without a startup hook, and the first press used to record “off” and change nothing; now one press always changes the sidebar.
Details
The state in net-view.json in the plugin’s state directory (“on” or “off” and the identity of the running server, the socket file’s inode and creation time) is only the wish the hooks read. The next press clears the view with agent.view.clear named to this plugin’s own source, so it never clears a view that another owner installed. A restarted server drops its view; the startup hook turns the view on and records it, so the first press switches it off.
[[events]] on = "pane.agent_detected" runs net-view.mjs reapply (Herdr has no hook for a link or an enable): it installs the view when no view is active and this server’s own “off” has not been pressed, and leaves any active view, this plugin’s or another owner’s, and a pressed off alone; so after a relink the net view is back at the next agent Herdr detects. A view another owner installed over this one also counts as off: the press installs this one again.
Limits
Section titled “Limits”Herdr keeps one Agents view per server, so another plugin’s view and this one replace each other. Unlinking or disabling the plugin while the view is on drops the view at once; the first press after linking it again only resets the state, and the second turns the view on. Herdr starts plugin commands with the PATH of the process that started its server, and a Herdr launched from a macOS app has only /usr/bin:/bin:/usr/sbin:/sbin. run.sh therefore looks for node on PATH, in Homebrew’s, /usr/local, mise, asdf, and volta directories, and through a login shell, and says so in the plugin log (herdr plugin log list --plugin wayne930242.straw-boss) when it finds none.