ctsheehan.com / writing
Agent orchestration

How I actually run fleets of coding agents

Fan-out, pipelines, adversarial verification, and the failure modes that cost me real time. Written for people who already use Claude Code, Codex or Cursor and want to go from one agent to many.

Most people using Claude Code or Codex or Cursor are running one agent in one chat, watching it work, correcting it when it drifts. That works. It also caps out fast, because you are the bottleneck: one agent can only be as parallel as your attention.

Going from one agent to many is not mostly a prompting problem. It is a setup problem, a topology problem, and a verification problem, roughly in that order. Here is how I run it, what the shapes actually are, and the failure modes that cost me real time. The failures are the useful part.

Part 01

Get the setup right before the fleet is worth anything

Everything I run sits on a small set of always-on rules plus a much larger set of lazy-loaded skills.

The always-on layer is deliberately tiny. My global config file is 182 lines and it is an index, not a manual. Alongside it live six rule files that load into every session no matter what:

~/.claude
$ wc -l CLAUDE.md rules/*.md
     182 CLAUDE.md
      60 rules/api-over-browser.md
      17 rules/bash-write-verification.md
      22 rules/known-issues-protocol.md
      82 rules/session-continuity.md
      31 rules/ship-sequence.md
      26 rules/worktree-isolation.md
     420 total

420 lines of always-on context, total. Everything else is loaded on demand.

Everything else is a skill: a directory whose one-line description is the only thing in context until the skill actually fires. There are 27 of them. My Linear skill is 1,463 lines across ten files, and the 64-line SKILL.md is what gets read when it triggers. The other 1,399 lines are pulled in only when a specific sub-task needs them.

~/.claude/skills
$ ls -1 | wc -l
      27
$ wc -l linear/*.md
     379 linear/agent-sdk.md
     115 linear/awaiting-chris.md
     228 linear/features-underused.md
     172 linear/hygiene.md
      78 linear/mcp-pacing.md
      77 linear/personal-hq-github-sync.md
     112 linear/readability.md
     129 linear/scan-before-creating.md
      64 linear/SKILL.md
     109 linear/workflow-states.md
    1463 total

One skill, ten files, 1,463 lines. Only the highlighted 64 load when it triggers.

That split is the whole game. Context is the scarcest resource in a fleet, and it gets scarcer the more agents you run, so almost everything should be lazy. But there is one rule about which things are allowed to be lazy, and I learned it by getting it wrong twice:

A guardrail has to load BEFORE the mistake it prevents. If it is lazy-loaded, it never fires in time.

I had written a rule about how worktree isolation actually behaves, and filed it inside a skill about machine infrastructure. That skill's description covers launchd agents, MCP scoping and dead daemons. It says nothing about git. So the skill never auto-triggered at session start, my main config file still carried the stale instruction, and an agent following that instruction reproduced the exact wedge the rule had been written to prevent.

Same shape a second time, with a rule about not chaining PR creation, merge and worktree cleanup into one shell command. Filed in a lazy skill. Never loaded at the moment it was needed.

Both are always-on now. The test I use: if the failure mode is silent or destructive, the rule is always-on. If it is reference material you look up once you already know you need it, it is a skill.

Part 02

Fan-out, and why agents should look at their own output

The most common shape is the simplest one. Take N independent pieces of work, hand each to its own agent, merge. The part most people skip is the small loop inside each branch.

task 1 brief agent 1 2 concepts agent 2 2 concepts agent 3 2 concepts agent 4 2 concepts render → look → critique own scratch dir render → look → critique own scratch dir render → look → critique own scratch dir render → look → critique own scratch dir 8 outputs one pass
swipe the diagram sideways →
Fan-out with a self-check inside each branch. The critique happens in parallel, for free, instead of serially at the end.

Last week I needed a LinkedIn banner. Instead of iterating with one agent, I ran four in parallel, each asked to produce two distinct concepts, render its own output headless, look at the resulting image, and critique it before reporting back. Eight designs, one pass, and this is genuinely what came out:

banner-gallery.html8 concepts / 4 agents / scroll
A review gallery of eight LinkedIn banner concepts produced by four parallel agents, each shown at true 1584x396 proportions
The real artifact. Each agent also built the review page's overlays (profile-photo position, mobile crop) so the output could be judged the way it would actually be seen.

Two of them found real bugs in their own work, and both are worth describing because of what kind of bugs they are.

The first had added a dither layer to kill banding across a wide gradient. It rendered the banner, looked at the PNG, and noticed the whole canvas had gone dull. The dither was a flat mid-grey texture composited on multiply, so it was not adding noise, it was multiplying everything by about 0.5 and desaturating the entire image. Its fix, and the comment it left behind in the file:

banners/warm-a.html
/* Zero-mean dither: kills banding across the wide ramp, shifts no colour */
.bnr-warm-a .dither{
  position:absolute; inset:-10%;
  opacity:.13; mix-blend-mode:overlay; pointer-events:none;
}

The second was trying to put a cool blue accent on a warm ivory ground and could not get the blue to read as blue. It worked out why:

banners/warm-b.html
/* The whisper of cool: a pale blue tint, added as light so it cools without muddying.
   Multiplying blue over warm ivory computes to dead neutral grey, so this sits normal. */
.bnr-warm-b .cool{
  position:absolute; inset:0;
  background:
    radial-gradient(800px 600px at -3% 26%,
      rgba(199,216,246,0.36) 0%,
      ...

Look at what those two have in common. Neither is visible in the source. The CSS is valid, the intent is legible, and a reviewer reading the diff would sign off on both. They exist only in the pixels.

An agent that renders and looks at its own output catches a class of error that an agent reading its own code structurally cannot.

That generalises well past design. An agent that runs the test suite catches what an agent that reads the test file does not. An agent that curls the endpoint catches what an agent that reads the route handler does not. Make "look at the artifact" part of the agent's own job rather than a step you do afterwards, and each branch of the fan-out gets a lot more useful.

Part 03

Pipeline beats barrier almost every time

Once each unit of work has more than one stage (write, then check / research, then verify / draft, then edit) you have a choice that most people get wrong by default.

The default is a barrier. Run stage one on all N items, wait for all of them, run stage two on all N. It reads naturally and it is what a Promise.all in a loop gives you. It is also slow, because every stage waits on its slowest member, and the slow member is a different item in each stage.

The alternative is a pipeline: each item flows through its own stages without waiting for its siblings. Item three can be getting adversarially checked while item one is still being drafted.

stage 1 (write) stage 2 (verify) Barrier every item waits for the slowest sibling barrier item 1 item 2 item 3 item 4 14 units Pipeline each item flows through on its own item 1 item 2 item 3 item 4 11 units where the barrier finished
swipe the diagram sideways →
Same four items, same durations, 21% less wall clock. With a barrier the total is the sum of each stage's slowest member. With a pipeline it is the slowest single item's whole path.

With variable durations, which is always, the pipeline wins and it never loses. In practice the call takes the item list plus one function per stage and threads each item through:

workflow.js
// Each topic flows through analyze then challenge on its own.
// No item waits for a sibling.
const done = await pipeline(TOPICS,
  (t) => agent(`Research ${t.title}. Write ${W}/${t.key}.md.`,
    { label: `analyze:${t.key}`, effort: 'high', schema: ANALYSIS }),
  (res, t) => agent(`Adversarially check ${W}/${t.key}.md. Is any number
    unsupported? Is the conclusion too strong? Rewrite it in place.`,
    { label: `challenge:${t.key}`, effort: 'high', schema: CHECK })
    .then(v => ({ key: t.key, res, check: v })))

The barrier is genuinely required in three cases, and it is worth knowing them so you can tell when you actually need one:

  1. Dedup across the full set. You cannot dedupe an item against results that do not exist yet.
  2. Early exit on zero. If stage one finds nothing, you want to skip stage two entirely rather than pay per item to discover that.
  3. Cross-item comparison. A judge that ranks all eight, picks a winner, or diffs one against another needs all eight in hand.

Everything else, pipeline it.

Part 04

Verification has to be adversarial, and diverse

Asking an agent "is this correct?" gets you "yes" far more often than it should. Agreement is cheap. So the verify step is prompted to refute, and the claim dies on majority refute rather than surviving on majority confirm.

The upgrade on plain N-refuters is perspective diversity: give each verifier a different lens instead of the same instruction N times. N identical skeptics correlate their errors, which is exactly the failure you were buying insurance against. Different lenses do not.

claim from stage 1 lens 1  check every number lens 2  quote the primary source lens 3  is the claim too strong lens 4  what argument is missing tally majority refute? kill keep
swipe the diagram sideways →
Perspective-diverse refutation. Four different questions, not the same question four times.

The reason I run it this hard: a research agent once came back CONFIRMED on a biographical claim, with a real, working citation URL. The claim was wrong. The page at that URL said something else. It had cited a genuine source and confabulated its contents, which is close to the worst possible failure because every surface signal looks right. Since then "verify" in my prompts means pull the document and quote the line, never tell me whether it checks out.

Part 05

Loop until dry, do not guess a count

For discovery work where you do not know the size of the answer (how many matching files, how many candidates, how many broken links) do not pass a count. Loop: harvest a batch, dedupe against everything you already hold, and stop when a full pass adds zero new items.

A fixed count is a guess that is wrong in both directions. Too low and you silently truncate, which is the dangerous one because a truncated result looks exactly like a complete one. Too high and you burn agents on empty passes. The dry test is cheap, self-correcting, and gives you a real stopping condition you can log.

Part 06

The failure modes, which are the actual content

Everything above is the happy path. These are the things that cost me hours.

01Eight concurrent agents tripped a burst limit and all returned null in 33 seconds

Not a usage cap, not a quota. An account-level burst limit on concurrent starts. The tell is the shape of the failure rather than the error text: real work does not fail in 33 seconds, and it does not fail uniformly. Eight identical nulls means infrastructure, not eight bad prompts. The fix was three sequential waves of two, which cost about a minute of wall clock and made the whole thing reliable. Above roughly four wide, wave it.

02{...null} spreads to {}, and {} is truthy

This is the bug that made the burst limit expensive instead of merely annoying. The result-collection code ran .filter(Boolean) over the agent results, which looks correct and is completely useless here, because spreading a null result into an object literal produces an empty object that sails through the filter. So I got an array of the right length full of nothing, and the synthesis stage downstream cheerfully wrote a confident document out of eight empty objects. Null-guard inside the .then, at the point the result arrives, not in a filter afterwards.

03A shared scratch directory made one agent screenshot another agent's work

Four agents, one tmp directory, each told to render its design and verify the image. They picked overlapping filenames. One agent's verification pass loaded, inspected and approved a PNG a sibling had just written. It reported a clean self-review, honestly, because the image it looked at genuinely was fine. It just was not its image. Every parallel agent gets a scratch directory keyed to its own label, always.

04Subagents do not start in the parent's worktree

They start in the primary project directory. So an agent handed a relative path edits a file in a different checkout of the same repo, the parent greps its own worktree, finds nothing, and you burn ten minutes working out which of you is hallucinating. Pass REPO_ROOT=$(git rev-parse --show-toplevel) explicitly and require absolute paths everywhere. This gets worse the more checkouts you keep alive, and running a fleet means keeping a lot alive:

one repo, mid-fleet
$ git worktree list
/Users/chris/Developer/schoolwise               ef2d7322 [fix-migration-version-collision]
/Users/chris/Developer/schoolwise-rc            95b3e848 [main]
/Users/chris/.../issue-sch-460-june22-fix       be202c9e [fix/sch-463-prompt-june11-authoritative]
/Users/chris/.../sch-734-stale-001-determinism  7477af75 [sch-734-stale-001-determinism]
/Users/chris/.../sch-data-completeness2         63a4b515 [sch-data-completeness2]
/Users/chris/.../sch485-live-eval               a0a0b5a7 [sch485-live-eval]
/Users/chris/.../sch485-verify                  6558d700 [sch485-verify]
/Users/chris/.../sch557-wa-debounce             cf805bfa [fix/sch557-wa-debounce]

05An unconditional success message after a && chain will lie to you

A write like cd <dir> && cat >> file <<EOF ... EOF with echo "done"; tail -3 file on the next line: if the cd fails, the && short-circuits, the heredoc never runs, and the echo and the tail both run anyway, printing a confident success and tailing a different copy of the file. In a fleet the working directory is not what you assume, so this fires far more often than it does interactively. Absolute paths for writes, confirmation inside the chain, and verify a write by grepping for the new text rather than tailing.

06Do not chain PR creation, merge and cleanup into one command

Each is network-bound and can take 10 to 60 seconds. Together they blow a two-minute timeout partway through with no signal about where. The damage is worse than a plain failure: if the trailing cleanup ran but the create did not, the branch and the worktree are gone while no PR was ever opened, so the work looks like it evaporated. Separate calls, and a timeout tells you exactly where you stopped.

07A fleet you cannot tell apart is a fleet you stop watching

Past about four concurrent sessions the bottleneck stops being the agents and becomes finding the right window. Terminals make this harder than it sounds, because most of them derive the window title from whatever tab is active, so the moment an agent updates its own title your carefully named window is gone. In WezTerm even wezterm cli set-window-title does not hold: the next title the tab emits recomputes it. What works is persisting the label per window id and returning it from a format-window-title handler, which always wins, then colour-coding the title bar per project on top. That turns "which one of these was schoolwise" into a glance. The forty lines are here.

Where to start

If you are running one agent today

Three things, in this order.

Split your config into always-on rules and lazy skills, and be honest about which failures are silent. Silent and destructive go always-on, everything else is a skill. A fleet magnifies whatever your setup already does, gaps included.

Make every agent verify its own artifact before it reports. Render it, run it, curl it, read it back. This is the change that pays most, because it moves verification out of a serial step at the end and into work that happens in parallel for free.

Pipeline instead of barrier, and wave your fan-out at about four wide. Then read your failures carefully. A fleet fails in ways a single agent does not, mostly around shared state, uniform errors, and results that are the right shape but empty.

Some of the pieces I use for this are open source, if you want to see the skill format up close:

If you are doing this inside a team and want a hand, email me.

Get in touch
chris@ctsheehan.com
← Back to ctsheehan.com