Draws the prompts herdr-agents submits (worker reports, decisions, context notices) as one line; ctrl+o shows them in full.

Run a team of coding agents from one Claude Code session. The coordinator plans the work, starts workers (Claude, opencode, codex) in herdr tabs, and their reports come back to it as prompts. You answer the questions only you can answer from a panel above the prompt.

Install it from the plugin marketplace, at a Claude Code prompt (details under Install):
/plugin install herdr-agents --marketplace contursofran/claude-herdr-orchestrator
The coordinator starts each worker in its own herdr tab, usually in its own git worktree, with a profile: the built-in planner, builder, reviewer and debugger, or your own on Claude, opencode or codex (smallTasks sends the small ones to a cheap model). A worker's first-run "trust this folder?" prompt is answered for it. Each worker's tab gets HERDR_AGENTS_ROLE=worker; the mod also loads in a Claude worker's own session, which runs none of its timers; for the workers it started, it sweeps and checks their reports only while one of them runs.
When a worker finishes, its RESULT.md comes back to the coordinator as a prompt; no polling, no copying. It shows in the chat as one dim line (ctrl+o for the full report). Workers start RESULT.md with a short header (status, PR, a 3-line summary); the coordinator gets the summary and the file's path, and reads the rest only when it needs to. Reports without a header are shortened to a pointer when long. herdr tells the mod the moment a worker ends its turn (herdr agent wait), and a RESULT.md is delivered within 3 seconds of landing, even while its worker is still finishing its turn (a worker with messages held for it waits for the end of its turn). A sweep each minute, with one herdr agent list for all workers, catches anything missed; a single failed herdr call never ends a job. Reports, your answers and ticked to-dos that arrive within a few seconds, or while the coordinator is busy, reach it as one prompt.
What only you can do (test a build, merge a PR, restart something) is pinned at the top of the panel, so it doesn't scroll away in a report. Tick Done and the coordinator carries on from it; it unpins what is no longer needed.

Questions that are yours (a product rule, a design choice, an approval) wait on the panel with their options and the recommended one starred, and herdr pings you. Answer with 1–4 from an empty prompt, with alt+d and the arrows, or with a click; o gives your own answer. A #12 links to that issue or PR.

Workers ask with DECISION: lines. The coordinator settles the ones that aren't yours, and the panel shows each answer with a ✓ for ten minutes, so you can overrule it.
Role, time and state for each worker: working, waiting on you, done. Four fit in the compact view; Details shows them all. A worker waiting on CI or a deploy leaves a WAIT.json with a check to run; the mod runs it and wakes the worker when it passes, with no model running meanwhile (⏸ and what it waits for).

/hrd-side docks the same things beside the transcript, in grey and white. At the top are the issues, one row per issue or PR. The planner, the builders and fix rounds, and the reviewer of the PR a builder linked share that row; the same number in two repos is two rows. A row whose work joins another row (a review that started before the builder's report named its PR) says so in its slot until clear done. A row's number opens its issue or PR on GitHub. Each row has a mark for plan, build, review, CI and you: ✓ done, ◐ running, ⏸ waiting, · not started, ? or ☐ waiting on you, ■ blocked. CI comes from the PR's checks, read with gh every two minutes while the sidebar is open. What needs you sits at the bottom, level with the prompt, over a box of fixed height for the item you pick; with nothing waiting it stays empty. For a question the box shows the answers, the recommended one first. For a to-do it shows Done and any link in the text.
The sidebar's background is Claude Code's composerSidebarBackground theme color. To match a terminal's background, add a custom theme in ~/.claude/themes/ (for example { "name": "Dark", "base": "dark", "overrides": { "composerSidebarBackground": "#101010" } }) and pick it with /theme. The line between the chat and the sidebar is Claude Code's resize grip.
Rows never move on their own. An answered or finished row stays in its slot, struck through, until you press clear done. A new one takes the next free slot. Answering swaps the box's contents in place, and the box stays on the answer until you press next. While the sidebar shows, the panel above the prompt steps aside. To start every session with the sidebar instead of the panel, set Show workers in to the sidebar in /hrd-config ("view": "side").
The panel shows the coordinator's context (ctx 75k / 200k); the sidebar shows it as a share of that limit (ctx 75k 38%). Past resetAt (200k tokens) the coordinator writes its state to COORDINATOR-STATE.md and is compacted, then reads it back before the next report, so a session can run all day.
Planner, then builder, then reviewer; each fix round goes to a fresh builder, and the debugger takes over after two failed rounds. The coordinator is reminded when fewer than target workers run.
/hrd-wrap-up, or the panel's Wrap up button, starts no new workers and lets the running ones finish; herdr notifies you when the last one is done. Worktrees whose PR merged are removed every half hour, with their tabs and branches. A worker's tab closes once it has sat idle half an hour after its report, unless it has a question open; a follow-up after that goes to a fresh worker.
/hrd-config lays the settings out above the prompt, like Claude Code's /config: for all folders or just this one.

What the sidebar shows is also kept in ~/.herdr-agents/board/<session id>.json, rewritten when it changes and removed when the session ends, so an app outside Claude Code (such as contr, a native herdr client) can draw it. The app presses the sidebar's buttons by dropping a JSON file in <session id>.actions/ beside it: answer a decision, tick or dismiss a to-do, clear done, wrap up or keep going. The coordinator gets the same prompts as from a click in the sidebar. The format is in docs/board-schema.json.
herdr-agents (the mod): the spawn, message and stop tools for workers, ask_owner, decided, pin_for_owner and unpin_for_owner for the panel, and everything above.herdr-agents-view (view/): draws worker reports as one dim line. It has to be a separate mod, because Claude Code never raises a mod's own ui.render hooks for the prompts that mod submits.agents/, shipped with the mod as herdr-agents:planner, herdr-agents:builder and herdr-agents:reviewer): planner (Opus high), builder (Haiku xhigh) and reviewer (Opus high); the debugger profile is Opus with full tools. They hold no repo rules of their own: they read the repo's AGENTS.md/CLAUDE.md and its .claude/skills checklists.You need herdr and Claude Code. At the prompt of a Claude Code session:
/plugin install herdr-agents --marketplace contursofran/claude-herdr-orchestrator
/plugin install herdr-agents-view@claude-herdr-orchestrator
Answer y to add the marketplace, then pick the user scope. The first installs the mod and its planner, builder and reviewer subagents; the second draws worker reports as one line. Start new sessions inside herdr to use it.
The first time the coordinator uses one of the mod's tools (spawn, ask_owner, …), Claude Code asks; choose "don't ask again", or allow them all at once in ~/.claude/settings.json:
{ "permissions": { "allow": ["mcp__herdr-agents"] } }
Optional: bind alt+d to move into the panel (ctrl+x tab works without it) in ~/.claude/keybindings.json:
{ "bindings": [{ "context": "Chat", "bindings": { "alt+d": "abovePrompt:focus" } }] }
Each release bumps the version in plugin.json. Run claude plugin update herdr-agents@claude-herdr-orchestrator (and the same for herdr-agents-view), or update them from /plugin, then start a new session. Watch the repository's releases on GitHub to hear about new versions.
templates/AGENTS.md to the repo's AGENTS.md (or CLAUDE.md) and fill it in: base branch, checks, how to update a branch, commit identity, who merges, what needs you. Every worker reads it, so this is where repo-specific rules go, not the subagents..claude/herdr-agents.json in the folder you coordinate from overrides ~/.claude/herdr-agents.json for that folder, e.g. {"target": 2}.claude --model opus --effort medium, then /hrd-orchestrate <goal>, e.g. /hrd-orchestrate the issues labelled ready. The session gets the coordinator playbook (state file, worktree per worker, planner → builder → reviewer, when to ask you) and starts workers./hrd-wrap-up (or the panel's Wrap up button) starts no new workers and lets the running ones finish; you get a herdr notification when the last one is done. /hrd-wrap-up off (Keep going) resumes.One coordinator per folder: its notes live in COORDINATOR-STATE.md there, and wrap-up applies to the coordinators of that folder.
All start with hrd- (commandPrefix changes it).
| Command | What it does |
|---|---|
/hrd-orchestrate <goal> | Make this session the coordinator, with the playbook and a goal. |
/hrd-wrap-up, /hrd-wrap-up off | Stop starting workers and let the running ones finish; resume. |
/hrd-side | Open or close the sidebar beside the chat. ctrl+x tab moves into it; there Tab walks its buttons and Enter presses one. It draws no hotkeys, so it stays grey and white. |
/hrd-decisions | List the questions and to-dos waiting for you. Answer on the panel: 1–4 from an empty prompt, or alt+d then arrows and Enter. |
/hrd-workers | List this session's workers. |
/hrd-prune | Remove worktrees whose PR is merged (also runs every pruneMinutes). |
/hrd-update | Pull the clone this mod was installed from (git pull --ff-only), then run its install.sh again. It answers with the commits and version; running sessions reload the new version by themselves. |
/hrd-config | The settings, laid out like /config, in the panel above the prompt: alt+d to move into them, ↑↓ to move, Enter to change (a switch flips, a choice steps on, a text opens a field), Done or /hrd-config again to close. The first row picks the file: all folders or this folder. Profiles are edited in the file. |
/hrd-help, /hrd-h | A short guide: the commands, the panel's keys, and the profiles and settings in effect for this folder. |
Change them with /hrd-config, or by hand in ~/.claude/herdr-agents.json (in $CLAUDE_CONFIG_DIR when set), with <folder>/.claude/herdr-agents.json over it (profiles merge by name):
| Key | Default | Meaning |
|---|---|---|
target | 4 | Workers the coordinator keeps busy. |
resetAt | 200000 | Context tokens past which a coordinator saves its state and is compacted. |
statePath | COORDINATOR-STATE.md | The state file, relative to the folder (or absolute, or ~/…). |
trustRoots | the folder and its parent | Folders where a worker's first-run "trust this folder?" prompt is answered for it. |
commandPrefix | hrd- | What every slash command starts with. Read when the session starts. |
profiles | see below | Worker profiles by name; one here replaces the built-in of that name. |
disable | none | Built-in profiles to turn off: any of planner (the builder plans first), reviewer (the coordinator reviews the PR itself), debugger (after two failed fix rounds the coordinator asks you). The builder always runs. |
smallTasks | none | A profile the coordinator may give small, well-defined tasks, e.g. "deepseek". |
playbook | none | A line, or a list of lines, added to the coordinator playbook, e.g. "Label every PR with the issue's area." |
pruneMinutes | 30 | How often merged worktrees are pruned; 0 turns it off (/hrd-prune still works). |
closeIdleMinutes | 30 | How long a worker's tab stays open, idle, after its report; 0 keeps it open. |
repos | from git remotes | Short names for #123 links, e.g. {"web": "owner/website"}. The repos of the session's and workers' folders are known already, by name and by the part after the last dash (acme-client, client). |
wrapUp | off | true wraps up every coordinator on the machine; /hrd-wrap-up off clears it. |
view | band | Where the workers and what needs you show: band above the prompt or side, the sidebar, opened at the start of each session. Only one shows; /hrd-side switches for the session. |
Built-in profiles, the playbook's steps: planner, builder, reviewer (the subagents above) and debugger (Opus medium, full tools, for root-cause hunts and fixes that failed twice). A profile of the same name under profiles changes its model or flags and keeps its place in the playbook; /hrd-help marks it customized. Any other name is a profile of your own. A profile is {"kind": "claude" | "codex" | "opencode", "args": [...], "env": {...}, "note": "..."}; kind is the CLI herdr starts and args its flags. Other CLIs or models are a profile away:
{
"profiles": {
"deepseek": {
"kind": "opencode",
"args": ["--standalone"],
"env": { "OPENCODE_CONFIG_CONTENT": "{\"model\": \"opencode-go/deepseek-v4.1-flash\"}" },
"note": "opencode DeepSeek V4.1 Flash"
},
"codex": { "kind": "codex", "args": ["-m", "gpt-6.1-sol", "--approve-for-me"], "note": "codex GPT-6.1 Sol" }
},
"smallTasks": "deepseek",
"disable": ["debugger"]
}
To run the mod from a clone instead of the marketplace (uninstall the marketplace copy first: two plugins of one name don't load):
git clone git@github.com:contursofran/claude-herdr-orchestrator.git
cd claude-herdr-orchestrator && npm install
./install.sh && npm run types # once Claude Code has loaded the mod: the plugin API's types
npm run check # formatting, type check, both manifests, tests
install.sh copies both mods to ~/.claude/mods/, creates ~/.claude/herdr-agents.json if missing, and adds to ~/.claude/settings.json and keybindings.json what loads the mods (CLAUDE_CODE_PLUGIN_DIRS), allows their tools and binds alt+d. After the first install, start new Claude Code sessions: /reload-plugins does not pick up new plugin folders. From then on, running sessions reload each update by themselves, and /hrd-update pulls and installs from inside a session.
npm run check runs format:check, typecheck, validate and test, each also on its own; run it before opening a pull request. CI runs the same on every pull request except typecheck, which needs the API types a local Claude Code writes. main takes changes only through pull requests, and only once CI passes. After a change, run ./install.sh again and bump version in .claude-plugin/plugin.json so running sessions reload the mod and marketplace users get the update. For a version worth announcing, tag it and publish a GitHub release saying what changed (gh release create v<version>).
hooks/register.tsx wires the engine's events to the modules beside it:
| Module | What it does |
|---|---|
config.ts | The settings files, the built-in profiles, the state file's path. |
defaults.ts | Default settings and tuning values. |
workers.ts | spawn, message, stop: starting workers, watching them, delivering RESULT.md, WAIT.json waits. |
report.ts | The report header workers write at the top of RESULT.md, and its parsing. |
owner.ts | Questions (ask_owner, DECISION: lines) and to-dos (pin_for_owner) waiting for the person. |
outbox.ts | Holds the coordinator's prompts and sends the ones that arrive together as one. |
coordinator.ts | The playbook, the worker target, wrap-up, and compacting the coordinator's context. |
target.ts | The line after a report or a stop: how many workers are still to start, or that wrap-up holds new ones. |
board.ts, band.tsx | The panel above the prompt: what it shows, and drawing it. |
side.tsx, tasks.ts | /hrd-side: the sidebar, and grouping workers into one row per issue or PR. |
links.ts | #123 links to the right repo, for the panel and the sidebar. |
settings.tsx | /hrd-config. |
commands.ts, tools.ts | The slash commands and the coordinator's tools. |
worktrees.ts, herdr.ts | Pruning merged worktrees; the herdr CLI. |
Each module keeps its own state (atoms in $.state) and offers functions to the others. Tests live in tests/, one file per area over a shared harness.tsx.
Ideas, issues and pull requests are welcome. Run npm run check before opening a PR.
MIT; see LICENSE.
hooks/register.tsx 22 lines1import type { Register } from 'claude-code'
2import { oneLine } from './format'
3
4// Claude Code never raises a mod's own ui.render hooks for the prompts that mod
5// submits, so herdr-agents can't shorten its own rows; this separate mod does.
6const SOURCE = 'herdr-agents'
7
8export const register: Register = on => {
9 on('ui.render', { component: 'UserMessage' }, async ($, e, next) => {
10 const origin = e.props.origin
11 if (e.props.isExpanded || origin?.kind !== 'plugin' || origin.name !== SOURCE) return next(e)
12 const line = oneLine(String(e.props.text ?? ''))
13 if (line === undefined) return next(e)
14 const { Text } = $.ui.resolve(e)
15 return (
16 <Text dimColor wrap="truncate-end">
17 {line}
18 </Text>
19 )
20 })
21}
22hooks/format.ts 76 lines1// The row's text with the engine's framing ("The herdr-agents plugin sent a
2// message:" and the closing note) left out: from the first <herdr-...> tag on.
3function payload(text: string): string | undefined {
4 const at = text.indexOf('<herdr-')
5 return at < 0 ? undefined : text.slice(at)
6}
7
8function attr(tag: string, name: string): string | undefined {
9 return tag.match(new RegExp(`${name}="([^"]*)"`))?.[1]
10}
11
12// The worker count, or the wrap-up note, that a prompt with a worker's report ends with.
13function workerNote(body: string): string {
14 const count = body.match(/\[(\d+) of (\d+) workers working/)
15 const wrap = body.match(/\[Wrapping up: (?:(\d+) workers? still out|every worker has finished)/)
16 return wrap ? ` · wrapping up, ${wrap[1] ?? 0} left` : count ? ` · ${count[1]}/${count[2]} working` : ''
17}
18
19// One element's line, from its tag (its first line) on, or undefined for a tag it doesn't know.
20// `note` is the worker note; a worker's line shows it.
21function element(part: string, note: string): string | undefined {
22 const lines = part.split('\n')
23 const tag = lines[0]!
24 const inner = lines
25 .slice(1)
26 .map(l => l.trim())
27 .filter(l => l && !l.startsWith('</herdr-'))
28 if (tag.startsWith('<herdr-agent ')) {
29 const first = inner.find(l => !l.startsWith('[')) ?? ''
30 const full = inner.some(l => l.startsWith('[Full report')) ? ' · full report in file' : ''
31 const pr = attr(tag, 'pr')
32 const prs = pr ? ` · PR #${pr.split(',').join(', #')}` : ''
33 return `⇠ ${attr(tag, 'name') ?? 'worker'} ${attr(tag, 'result') ?? attr(tag, 'status') ?? ''}: ${first}${full}${prs}${note}`
34 }
35 // Single-line forms: <herdr-decision ...>text</herdr-decision>, <herdr-context ...>text...
36 const text1 =
37 tag
38 .replace(/^<herdr-[a-z]+[^>]*>/, '')
39 .replace(/<\/herdr-[a-z]+>$/, '')
40 .trim() ||
41 inner[0] ||
42 ''
43 if (tag.startsWith('<herdr-decision ')) {
44 const chose = inner
45 .find(l => l.startsWith('The owner chose:'))
46 ?.slice(16)
47 .trim()
48 const question = inner
49 .find(l => l.startsWith('Question:'))
50 ?.slice(9)
51 .trim()
52 return chose ? `◆ ${attr(tag, 'id') ?? ''} owner chose: ${chose} · ${question ?? ''}` : `◆ ${text1}`
53 }
54 if (tag.startsWith('<herdr-owner-done ')) {
55 const did = inner
56 .find(l => l.startsWith('The owner ticked off:'))
57 ?.slice(21)
58 .trim()
59 return `✓ ${attr(tag, 'id') ?? ''} owner did: ${did ?? text1}`
60 }
61 if (tag.startsWith('<herdr-context')) return `↻ herdr-agents: ${text1}`
62 return undefined
63}
64
65// One line for a herdr-agents prompt, or undefined to keep the engine's row. A batch (see
66// outbox.ts) is several elements: one line for each, and the worker note once after them.
67export function oneLine(text: string): string | undefined {
68 const body = payload(text)
69 if (!body) return undefined
70 const parts = body.split(/\n(?=<herdr-)/)
71 const note = workerNote(body)
72 if (parts.length === 1) return element(body, note)
73 const lines = parts.map(part => (element(part, '') ?? part.split('\n')[0]!).replace(/^⇠ /, ''))
74 return `⇠ ${parts.length} updates: ${lines.join(' · ')}${note}`
75}
76