Quartermaster: one conversation, many agents
Running one agent is easy. You talk to it, it does the work, you review it.
Running ten is different. Each one needs its own branch and worktree so they don't trip over each other. Each one needs a brief. Each one gets stuck in its own way. One hits a failing check to fix. One needs me to run a command it isn't allowed to run. One finishes and sits there idle. Another finishes and needs cleaned up.
I was the scheduler, the pager, and the memory for all of them. That's the part a computer should do.
I run my agents in Herdr, a terminal that knows which panes are agents and what each one is doing, and track work in beads, an issue tracker that many agents can update at once. On top of those I tried the obvious options. Minding sessions by hand stopped scaling around a dozen. A dashboard showed me the state of every agent, but seeing isn't coordinating. A full orchestrator was a whole world to buy into. The closest was herdr-projects: one coordinator, a worker per task, a sidebar of what needs you. But it kept its own state in Markdown files, and for state that many agents read and write at once, beads is the better tool. A few other rough edges and I knew I'd only be happy with a setup I wrote myself.
So I wrote quartermaster. It keeps herdr-projects' shape, puts all the state in beads, and runs as much in parallel as I'll let it.
What quartermaster is
Quartermaster is a Herdr plugin with a CLI, quartermaster, or qm for short. It does four things.
One place to coordinate from. I talk to one agent, the coordinator. I tell it what I want. It turns that into beads, proposes how to split the work, and waits for me to say go. It never does the work itself, so it's always free to answer me.
One bead, one branch, one worktree, one agent. When I say go, quartermaster claims the bead, creates a worktree on its own branch, starts Claude or Codex in it named after the bead, and sends it a short brief. Starting a thread is one key. Cleaning up after a merge is one command.
Workers get checked on automatically. A background ticker follows every thread. When a PR's checks fail, it tells the worker. When review comments land, it tells the worker. When a PR is ready to merge, it tells both of us. When it merges, it tells the worker to verify and close the bead. Every hour it also rechecks beads nobody has touched in a few days, in case their work is already done. It follows deploy workflows linked in the bead too. It only types into an agent that's been idle for a minute, so it never lands in the middle of something I'm typing.
Beads is the only state. Quartermaster keeps no task list of its own. Statuses, notes, questions and lessons live in beads. That's what makes running many agents at once work: they all read and write the same record. It's also why nothing is lost when an agent or the whole terminal restarts. The next session picks up exactly where things are.
The sidebar puts the coordinator on top with a summary of what's waiting, then every thread sorted so what needs me comes first. Each worker is named after its bead, so every row says which task it is and where it stands:
quartermaster coordinator · 2 need you · 1 review · 3 working · 2 inbox app-7qd needs you · run command app-k2m checks failing · PR #405 (integration-tests) app-9xr-2 review · PR #411 approved · ENG-1024 app-3fw rolling out · PR #398 · deploy running app-p1c working app-c8n-1 idle
How it works
The pieces
| Piece | What it is | What it owns |
|---|---|---|
| Beads | The issue tracker in the repo | All task state: status, notes, questions, lessons. The only record. |
| Herdr | The terminal the agents run in | Panes, agent names and states, worktrees, the sidebar, notifications |
| Coordinator | One agent named quartermaster, in its own folder | Talking to me, planning, creating beads, dispatching. Never does the work. |
| Workers | One agent per bead, named after it, in its worktree | Doing the work and keeping their bead current |
| Ticker | A background loop, started with the plugin | Watching threads, following PRs and runs, prompting workers, the inbox, the sidebar |
The pieces never talk to each other directly. A worker says everything through its bead: notes, a PR link, a question, a command it needs me to run. The ticker reads beads, Herdr and GitHub, and writes what changed to an inbox, which is just a folder of files. The coordinator reads the inbox when it's nudged, or when I ask. The state lives in beads and those files. A prompt only says "go look", so if one gets missed, nothing is lost.
The loop
me ─ talk ─▶ coordinator ─ qm dispatch ─▶ worker
▲ ▲ │ │ ▲
│ │ └─ qm context ◀┐ │ │ prompts
│ notifies │ nudge │ inbox │ │
│ + sidebar │ │ ▼ │
└─────────── ticker ─────────────┴───────────┘
reads bd and herdr every 15s,
GitHub PRs and runs every 60s
- I ask the coordinator for something. It runs
qm context, creates beads for the work, with parents and dependencies, and proposes threads. Then it waits for my go-ahead. - The coordinator dispatches.
qm dispatch <bead>claims the bead, creates a worktree on its own branch, starts Claude or Codex named after the bead, and sends the brief: the bead and its title, the worktree and branch, the issue key if there is one, how to report, and my own standing instructions. If the agent opens on a startup prompt, like folder trust, the brief waits in an outbox and I'm notified. The ticker delivers it once the agent is ready. - The worker works and writes to its bead. Findings and decisions go in
bd note. It notesPR: <url>when it opens a PR, and any Actions run it's waiting on. It turns on auto-merge for each PR once it isn't a draft, so an approved PR with passing checks merges without anyone clicking merge. Who has to approve is still up to the repo's own rules. When it needs a decision, it writes the question and sets the bead toneeds_me. When it needs me to run a command, it writesRUN: <command>and does the same. If steps are left after a merge, like a deploy, it adds therolling-outlabel. - The ticker watches. Every 15 seconds it reads the active beads (in progress, needs me, blocked) and Herdr's agents, and joins them by name. Every 60 seconds it also asks GitHub about each bead's PR, found by branch or by a PR link in the notes, and any Actions runs linked in the notes. It compares each thread with what it saw last time.
The ticker acts on what changed.
- The sidebar: each worker's row gets its state line, like
review · PR #411 approved · ENG-1024, and what needs me sorts to the top. - The worker: failing checks, new review feedback, a PR ready to merge, auto-merge turned off, a merge or a finished run each become a prompt to that worker. GitHub turns auto-merge off when the base branch changes or someone without write access pushes, so while the PR is open, the worker is asked to turn it back on. Prompts wait until the worker has been idle for a minute, so they never land in the middle of something, and several are sent together.
- The inbox: each change is also an event file for the coordinator.
- Me: a thread needing me, a PR ready to merge, a merge, a failed run or a human review sends me a notification. Bot reviews go to the worker without bothering me.
- The coordinator: when there are new events and it's been idle for a minute, the ticker nudges it to run
qm context.
- The sidebar: each worker's row gets its state line, like
- The coordinator catches up. It reads the inbox and threads, relays questions and
RUN:commands to me, passes my answers to the worker, and marks the items handled. - The thread finishes. After the merge the worker verifies its work and closes its bead with a reason. The ticker sees the bead leave the active set and writes a
closedevent, and the coordinator runsqm resolveto remove the worktree.
Finishing a thread
Usually there's nothing for me to do. When a PR merges, the ticker tells the worker to verify and close its bead with what shipped and how it was verified. A rolling-out bead finishes its rollout first. Then qm resolve removes the worktree and the workspace, closing the worker's pane, and deletes the local branch if the PR merged. It refuses while the bead is still open or its agent is working, and it only ever removes that bead's own worktree, never the main checkout.
Other cases:
| Situation | What happens |
|---|---|
| Done without a PR, like an investigation | The worker closes its bead with a reason when it finishes; the coordinator runs qm resolve <id>, since auto-resolve only covers merged PRs |
| Dropping or parking the work | I tell the coordinator; it closes the bead (dropped: …) or defers it and resolves it, with --force only if nothing in the worktree is worth keeping |
| Done, but I want the worktree a while longer | I ask the coordinator to hold off on qm resolve |
| Finished worktrees have piled up | qm sweep, then qm sweep --yes to remove the safe ones |
| Claims nobody is going to finish | qm stale, then qm stale --release to reopen them |
Messages
Every automated message says it isn't from me, and none of them approves anything.
| From → to | When | Starts with |
|---|---|---|
| dispatch → worker | Once, at start | You are the quartermaster worker for bead … |
| ticker → worker | Checks fail, review, ready to merge, auto-merge off, merge, run done | [quartermaster: automated, not the user] PR #N …, or … <workflow> run <url> … |
| resume → worker | After qm resume | [quartermaster] You were resumed after your agent exited. |
| ticker → coordinator | New inbox items and the coordinator is idle | [quartermaster ticker: automated, not the user, approves nothing] … |
| worker → everyone | Any time | A note on its bead, or a status change |
| coordinator → worker | Relaying my answer | Whatever it writes |
A day with it
Here's what a day looks like. The details are made up. The shape is real.
Morning. I open the coordinator and ask where things stand. It reads the state and gives me three lines. One worker is asking whether to roll out to one region first or all at once. Overnight, one PR's integration tests failed, and the ticker already sent that worker the failure, so it's fixing it. Another PR merged, and its worker verified it and closed its bead.
I answer the one question. The coordinator passes it on. I haven't opened a single pane.
New work. I paste in my notes from yesterday's incident: a resolver got saturated, requests timed out, and our alerts said nothing because they watched errors, not demand. I want an alert on demand, a config fix, and a write-up for the postmortem.
The coordinator makes three beads, notices the write-up should cite the alert's threshold, adds a dependency, and proposes starting two now. I say go. Two workspaces open, each on its own branch.
Midday. The alert PR fails lint. I don't touch it. The ticker sees the failure, waits for the worker to go idle, and sends it the failing check. It fixes it and pushes. On the other PR, a teammate asks for the timeout to be configurable. The ticker hands the review to that worker, which makes the change and replies. I can see and follow all of this looking at the sidebar.
A command only I can run. One worker needs a production change its permissions won't allow. It writes the exact command on the bead and marks it as needing me. The sidebar goes red: needs you · run command. I copy the command from the board, run it, and tell the coordinator how it went. The coordinator passes the result to the worker, which notes it on the bead and continues.
Afternoon. The alert merges. The worker checks that the rule actually loaded, then closes its bead. That unblocks the write-up, which the coordinator mentions in its next status.
A restart. Herdr restarts, and it doesn't relaunch agents. The coordinator lists two threads as resumable. qm resume starts each worker again in its worktree, continuing its last conversation.
End of day. qm report writes my daily update: what shipped and how it was verified, what's in flight, what needs me. I ask the coordinator what's left, and I mark two beads for tomorrow. One lesson from the day goes into beads' memory, so every agent that starts tomorrow sees it.
Nothing about the day lived in my head. Tomorrow starts the same way: open the coordinator, ask where things stand.
How I built it
I built the first version in a day with Claude. It's a single Go binary. Herdr has a CLI and a socket API for panes, agents, worktrees and sidebar metadata, so quartermaster is mostly glue between Herdr, beads and gh.
The second round was more interesting. I asked Claude how quartermaster could fit the way I actually work. It looked at my beads, my running agents and what had come up that day, and came back with a list: resume agents after a restart, surface commands that only I can run, keep following work after merge, support several repos, keep Linear in step, write my daily report, clean up old worktrees.
Then it built all seven at once. Seven agents, each on its own branch in its own worktree, which is exactly what quartermaster is for. It merged them one at a time and ran the tests after each. Then an independent reviewer went over the combined change, and it caught two real bugs: cleanup could have deleted the worktree of a rollout that was still in progress, and an old command could have been offered to me to run again. Fixed and shipped.
That's the workflow in miniature. The agents did the work in parallel. The review caught what parallel work misses. I made the calls.
The sharp edges
- The coordinator proposes; it doesn't decide. It waits for me before starting threads. Agents are good at doing work and worse at knowing which work matters.
- Automated prompts need manners. The ticker only types into an agent that has sat idle for a minute. On the Herdr version I run, a prompt merges into whatever you've half-typed, and nothing annoys you faster than an agent that interrupts you.
- Cleanup is where you lose work. Removing a worktree is irreversible. Quartermaster keeps anything dirty, anything with unpushed commits, anything with an agent in it, and anything whose bead is still active. The dry run is the default.
- Names are the contract. Quartermaster ties a worker to its bead by its name. It names each worker itself when it starts it, and if a name drifts, the ticker finds the one agent in that bead's worktree and renames it back.
- Folder trust is per path. Claude asks whether to trust every new folder. Worktrees of a repo you've already trusted seem to be fine. For anything else, quartermaster holds the brief until you answer.
- It's built for how I work. One tracker, one terminal, Claude and Codex, GitHub. If your setup is different, the ideas probably carry over better than the code.
Why build it
What I needed was small. Start an agent in the right place. Check on it. Tell me when it needs me. Clean up after it. Keep the state where it already lives.
AI didn't make managing agents unnecessary. It made the agents good enough that managing them became the bottleneck. Quartermaster moves that job off me and onto a loop that doesn't forget, so the time I have goes to the decisions.
Quartermaster is on GitHub at travisjeffery/herdr-quartermaster. Installing takes two commands, with nothing to build:
herdr plugin install travisjeffery/herdr-quartermaster --ref v0.5.1
herdr plugin action invoke configure --plugin quartermaster # from a workspace in your repoBeads needs the needs_me status (bd config set status.custom needs_me), and the prefix+j and prefix+shift+j keys and the sidebar row go in ~/.config/herdr/config.toml. The README has the snippet.
It used to be called flatcircle, and before that kelpie and shepherd. flatcircle, kelpie and shepherd all still work as deprecated aliases.