Show which agent spawned which, from briefs already in the bus #14

Merged
forgejo-admin merged 2 commits from add-spawn-tree into main 2026-08-17 14:40:35 +00:00

Agents spawn agents spawn agents, and herdr renders them flat. After a few hops nobody can say who asked for what, or what a reap would take with it.

$ spawn tree
herdrmastr  w2F:p1  [working]
└─ herdrmastr/race-probe-0817  w6B:p1  [idle]
configure-ui/tango-ember-acl  w4Y:p5  [done]
celilo/fw-interface-classify  w5A:p1  [idle]
celilo/wireguard-peer-authority  w64:p1  [working]
├─ celilo/consumer-cleanup-hook  w65:p1  [idle]
│  └─ celilo/consumer-removal-cleanup  w6A:p1  [working]
└─ celilo/celilo-module-barrier-audit  w66:p1  [idle]

--subjects adds each agent's brief subject, which is usually the answer to "why does this agent exist":

celilo/wireguard-peer-authority  w64:p1  [working]
        celilo#765 then wireguard-manager
├─ celilo/consumer-cleanup-hook  w65:p1  [idle]
│       Design: providers clean up after a removed consumer (celilo#847)
│  └─ celilo/consumer-removal-cleanup  w6A:p1  [working]
│          Implement celilo#847 — providers clean up after a removed consumer

No new store

spawn already delivers every brief as mail from the spawning pane to the new one, so the parent edge has been recorded since the first spawn — nothing had ever read it. spawn_edges() recovers it by finding the briefs (identified by compose_brief's own footer) and reading each sender. 39 briefs are recoverable from recent history alone.

Two properties that matter more than the rendering

A reaped parent does not orphan its children. They re-attach to the nearest ancestor still running, so the tree keeps showing real relationships as intermediate agents come and go.

Pane ids are recycled after a reap, so the recorded graph can close a loop. Walking it terminates instead of hanging, and an agent is never shown as its own parent.

That second half was a real bug, not a hypothetical: the first version returned the starting pane as its own ancestor, and the selftest assertion caught it before it shipped. Newest-brief-per-pane wins for the same reason — an id seen today may belong to a different agent than the one that held it yesterday.

Also: a pane, so it can go on a hotkey

The tree is most useful as a glance rather than a command you stop to type, so the plugin ships it as a popup pane:

[[keys.command]]
key = "prefix+t"
type = "shell"
command = "herdr plugin pane open --plugin herdr-spawn --entrypoint tree"
description = "show the agent spawn tree (who spawned whom)"

Refreshed every 10s rather than herdr-mail's 3s — the genealogy only changes on a spawn or a reap, and each pass costs a bus scan plus an agent list.

The command is cd "${HERDR_PLUGIN_ROOT:-.}" rather than either alternative alone. A pane's cwd is the plugin root (mail's inbox pane already relies on that), but depending only on HERDR_PLUGIN_ROOT breaks if herdr doesn't set it for panes, and cd "" exits immediately — and a popup that closes at once is indistinguishable from one that never opened.

Checks

spawn selftest covers label derivation from a worktree path and its fallbacks, re-attachment through a dead parent, and the cycle guard. Verified the cycle gate fails when the guard is removed:

AssertionError: a cycle from recycled pane ids must not spin

Known limit: popups don't appear in herdr pane list or pane process-info, so the pane's rendering can't be verified from a shell — equally true of the long-standing mail inbox pane. What was verified is the exact command the manifest runs, with and without HERDR_PLUGIN_ROOT set.

🤖 Generated with Claude Code

Agents spawn agents spawn agents, and herdr renders them flat. After a few hops nobody can say who asked for what, or what a reap would take with it. ``` $ spawn tree herdrmastr w2F:p1 [working] └─ herdrmastr/race-probe-0817 w6B:p1 [idle] configure-ui/tango-ember-acl w4Y:p5 [done] celilo/fw-interface-classify w5A:p1 [idle] celilo/wireguard-peer-authority w64:p1 [working] ├─ celilo/consumer-cleanup-hook w65:p1 [idle] │ └─ celilo/consumer-removal-cleanup w6A:p1 [working] └─ celilo/celilo-module-barrier-audit w66:p1 [idle] ``` `--subjects` adds each agent's brief subject, which is usually the answer to "why does this agent exist": ``` celilo/wireguard-peer-authority w64:p1 [working] celilo#765 then wireguard-manager ├─ celilo/consumer-cleanup-hook w65:p1 [idle] │ Design: providers clean up after a removed consumer (celilo#847) │ └─ celilo/consumer-removal-cleanup w6A:p1 [working] │ Implement celilo#847 — providers clean up after a removed consumer ``` ## No new store `spawn` already delivers every brief as mail **from** the spawning pane **to** the new one, so the parent edge has been recorded since the first spawn — nothing had ever read it. `spawn_edges()` recovers it by finding the briefs (identified by `compose_brief`'s own footer) and reading each sender. 39 briefs are recoverable from recent history alone. ## Two properties that matter more than the rendering **A reaped parent does not orphan its children.** They re-attach to the nearest ancestor still running, so the tree keeps showing real relationships as intermediate agents come and go. **Pane ids are recycled after a reap, so the recorded graph can close a loop.** Walking it terminates instead of hanging, and an agent is never shown as its own parent. That second half was a real bug, not a hypothetical: the first version returned the starting pane as its own ancestor, and the selftest assertion caught it before it shipped. Newest-brief-per-pane wins for the same reason — an id seen today may belong to a different agent than the one that held it yesterday. ## Also: a pane, so it can go on a hotkey The tree is most useful as a glance rather than a command you stop to type, so the plugin ships it as a popup pane: ```toml [[keys.command]] key = "prefix+t" type = "shell" command = "herdr plugin pane open --plugin herdr-spawn --entrypoint tree" description = "show the agent spawn tree (who spawned whom)" ``` Refreshed every 10s rather than herdr-mail's 3s — the genealogy only changes on a spawn or a reap, and each pass costs a bus scan plus an agent list. The command is `cd "${HERDR_PLUGIN_ROOT:-.}"` rather than either alternative alone. A pane's cwd is the plugin root (mail's inbox pane already relies on that), but depending *only* on `HERDR_PLUGIN_ROOT` breaks if herdr doesn't set it for panes, and `cd ""` exits immediately — and a popup that closes at once is indistinguishable from one that never opened. ## Checks `spawn selftest` covers label derivation from a worktree path and its fallbacks, re-attachment through a dead parent, and the cycle guard. Verified the cycle gate fails when the guard is removed: ``` AssertionError: a cycle from recycled pane ids must not spin ``` **Known limit:** popups don't appear in `herdr pane list` or `pane process-info`, so the pane's rendering can't be verified from a shell — equally true of the long-standing mail inbox pane. What *was* verified is the exact command the manifest runs, with and without `HERDR_PLUGIN_ROOT` set. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Agents spawn agents spawn agents, and herdr renders them flat, so after a few
hops nobody can say who asked for what or what a reap would take with it.

`spawn tree` prints the genealogy. `--subjects` adds each agent's brief subject,
which is usually the answer to "why does this agent exist".

No new store: `spawn` already delivers every brief as mail FROM the spawning
pane TO the new one, so the parent edge has been recorded since the first
spawn and nothing has ever read it. spawn_edges() recovers it by finding the
briefs -- identified by compose_brief's own footer -- and reading each one's
sender.

Two properties that matter more than the rendering:

- A reaped parent does not orphan its children. They re-attach to the nearest
  ancestor still running, so the tree keeps showing real relationships as
  intermediate agents come and go.

- Pane ids are recycled after a reap, so the recorded graph can close a loop.
  Walking it terminates instead of hanging, and an agent is never shown as its
  own parent. That second half was a real bug: the first version returned the
  starting pane as its own ancestor, and the selftest assertion caught it before
  it shipped.

Newest brief per pane wins, for the same reason: an id seen today may belong to
a different agent than the one that held it yesterday.

Covered by selftest: label derivation from a worktree path and its fallbacks,
re-attachment through a dead parent, and the cycle guard. Verified the cycle
gate fails when the guard is removed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`spawn tree` is most useful as a glance, not a command you stop to type, so the
plugin now ships it as a popup pane and the skill documents the keybinding.

Refreshed every 10s rather than herdr-mail's 3s: the genealogy only changes when
an agent is spawned or reaped, and each pass costs a bus scan plus an agent
list. Polling three times faster would buy nothing.

The command is `cd "${HERDR_PLUGIN_ROOT:-.}"` rather than either alternative on
its own. A pane's cwd is the plugin root -- herdr-mail's inbox pane already
relies on that -- but depending only on HERDR_PLUGIN_ROOT breaks if herdr does
not set it for panes, and `cd ""` exits immediately. A popup that closes at once
is indistinguishable from one that never opened, which is a bad failure to debug
and an easy one to avoid.

Note popups do not appear in `herdr pane list` or `pane process-info`, so the
rendering cannot be verified from a shell -- this is equally true of the
long-standing mail inbox pane. What was verified is the exact command the
manifest runs, with and without HERDR_PLUGIN_ROOT set.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
forgejo-admin/herdrmastr!14
No description provided.