work · ai orchestration

fanout

stdlib python · zero install · github →

prose task orchestrator claude -p → json W1 · agent .done W2 · agent .done W3 · agent .done Wn · agent .done
one task, decomposed into n independent prompts, each in its own tmux pane. the parent never asks tmux anything, it just watches for the sentinel files to appear.

i wrote about conducting eight claudes by hand once. fanout is that essay turned into a tool: one bash command spawns n claude agents in n tmux panes, side by side, and either you hand it the splits or an orchestrator decides them for you. the whole thing is a nine-line bash shim around a few hundred lines of standard-library python. no pip installs, no config files, no ~/.fanout directory to clean up. that constraint is the design, not a flex, a tool you have to install is a tool you reach for less.

the orchestrator is an untrusted planner

the interesting command is fanout plan "task". you don't know how to split the work, so a single headless claude call reads the task and returns json: how many agents, why, and one self-contained prompt per agent. the prompt contract is strict, each prompt has to read cold with no awareness of the others, because the agents never talk to each other. but here is the part i care about: the code trusts none of it. the model says n and also emits a tasks array, and the two are cross-checked before anything launches.

data = _extract_json(out)
if not isinstance(data, dict) or "tasks" not in data:
    sys.exit(3)
if len(data["tasks"]) != data["n"]:   # the model contradicted itself
    sys.exit(3)

a language model is a planner that will occasionally hand you a beautifully formatted lie. so you treat its structured output the way you'd treat a form submitted by a stranger: parse defensively, validate the invariants, refuse on mismatch. the model proposes; the code decides whether the proposal is even internally consistent.

pulling json out of prose

models wrap json in apologies and markdown fences. the extractor first tries a plain parse, then scans for the first brace and walks the string balancing depth, but it tracks whether it's inside a string literal so a { sitting inside "use { here" doesn't throw off the count.

if in_str:
    if esc: esc = False
    elif ch == "\\": esc = True
    elif ch == '"': in_str = False
else:
    if ch == '"': in_str = True
    elif ch == "{": depth += 1
    elif ch == "}":
        depth -= 1
        if depth == 0: return s[start : j + 1]

it's a tiny state machine, maybe fifteen lines, and it's the difference between "the demo works" and "the demo works on the one input you tested." the bracket inside a string is exactly the case a regex gets wrong.

parallelism only pays while the agents stay strangers. the moment they read each other, you've coupled their assumptions and rebuilt a slow team.

coordinate through the filesystem, not through tmux

the move i'm proudest of is how a pane reports that it's done. the obvious approach is to ask tmux, scrape capture-pane, track pids, poll the session. all brittle. instead each pane pipes its output to a file and touches a sentinel when claude exits, and the parent just watches for the sentinel files to appear.

claude -p "$(cat W{i}.prompt)" --tools '' \
  | tee W{i}.out;          # stream output to disk
touch W{i}.done;           # the only signal the parent needs
echo '=== W{i} done. ctrl-b d to detach. ===';
sleep 86400                # keep the pane alive for inspection

now the orchestrator never speaks tmux's language. it polls a directory once a second. detach and reattach, resize, close your laptop, the contract is just "a file will exist when the work is finished." this is the unix instinct that ants have, by the way: an ant doesn't broadcast to the colony, it drops a pheromone on the ground and the ground becomes the shared memory. the filesystem is the pheromone trail here. and the sleep 86400 is mercy, it holds the pane open so a human can actually read what the agent did.

fan-out, honestly, with no fan-in

i want to be precise about what this is and isn't. it's pure fan-out. the orchestrator prompt forbids one agent from referencing another's output, and there's no reduce step that merges results, you get concatenated blocks. that's a deliberate limit, not an oversight: the second you let agents read each other, you've coupled their assumptions and lost the only thing parallelism was buying you, independence. when a task genuinely can't be parallelized the orchestrator is told to return n=1 and run it alone. there's also a graceful fallback, if tmux is missing it drops to asyncio.gather and runs the same agents concurrently in one terminal.

how this differs from claude's own fan-out

claude code can already run agents in parallel, the task tool spawns subagents and the model orchestrates them for you. fanout is a different shape on purpose. there, the orchestrator is the model and it decides and manages the whole thing inside one runtime; here, you are the conductor, and the orchestrator agent does exactly one job, draft the n prompts, which you read before anything launches. claude's subagents also fold their results back into the parent's context, so the parent re-reads everything and synthesizes; fanout is pure fan-out, the parent never ingests the children at all.

the deeper split is isolation and visibility. claude's subagents live inside the harness, managed and mostly opaque, and you get a summary at the end. fanout's agents are separate operating-system processes, each its own claude -p session with its own context window and its own token budget, laid out in tmux panes you can attach to and watch type in real time. and it's just bash around the cli, no sdk, no product, nothing to install or clean up. claude's built-in is more capable when you want synthesis and tool-mediated coordination. fanout wins when you want raw independence, a human in the conductor's seat, and the ability to literally watch eight machines work side by side.

the lesson that keeps proving itself is the boring one. the leverage was never the model being fast. it was deciding the cut lines before a single agent runs, freezing the contract, and refusing to ship a result you couldn't defend. fanout just makes those decisions cost one bash command instead of an afternoon. و من جدّ وجد, whoever strives, finds.

next · nanovsm → all work →