Agent Orchestrator: Git Worktrees Are the Secret to Running 23 AI Coding Agents in Parallel
Hook
Most developers run one AI coding agent at a time because managing multiple agents on different branches is a nightmare. Agent Orchestrator runs 23 different agents in parallel by treating git worktrees like lightweight VMs.
Context
The explosion of terminal-based AI coding agents—Claude Code, Aider, Cursor CLI, Codex—created a new problem: these tools are phenomenally productive individually, but they don't compose. Try running Aider on a feature branch while Claude Code fixes a bug on main, and you're manually juggling git checkouts, terminal windows, and branch state. Scale to three or four parallel tasks and you're spending more time on git housekeeping than actual development.
The typical solution is to just... not do it. Developers run one agent at a time, waiting for each to finish before starting the next. But this is absurdly inefficient when you have five straightforward bug fixes that could run in parallel, or want to explore three different implementation approaches simultaneously. Agent Orchestrator from Untrivial solves this by building a process supervisor specifically designed for terminal-based AI agents, using git worktrees to give each agent its own isolated workspace and tmux sessions to manage their lifecycles. It's not a multi-agent framework—it's infrastructure for running multiple single-purpose agents without them stepping on each other's toes.
Technical Insight
The architectural insight that makes Agent Orchestrator work is deceptively simple: git worktrees are better than branches for parallel agent work. A worktree is essentially a separate checkout of your repository in a different directory, with its own branch and working files. When you spawn an agent session, the orchestrator creates a dedicated worktree, launches a tmux session inside it, and starts your agent CLI there.
Here's what the worktree initialization looks like under the hood:
// Simplified from the actual codebase
func (s *Session) CreateWorktree(branchName string) error {
worktreePath := filepath.Join(s.baseDir, ".worktrees", s.ID)
// Create worktree with new branch
cmd := exec.Command("git", "worktree", "add",
"-b", branchName,
worktreePath,
"HEAD")
if err := cmd.Run(); err != nil {
return fmt.Errorf("worktree creation failed: %w", err)
}
// Spawn tmux session in the worktree
tmuxCmd := exec.Command("tmux", "new-session",
"-d", // detached
"-s", s.ID,
"-c", worktreePath) // working directory
return tmuxCmd.Run()
}
This sidesteps the entire class of problems around branch switching and working directory state. Agent A can modify files freely in its worktree while Agent B works in a completely separate directory on a different branch. No checkout conflicts, no "uncommitted changes" errors, no accidentally running commands in the wrong branch context.
The agent adapter system is equally pragmatic. Instead of reimplementing agent logic or maintaining forks, Agent Orchestrator just wraps existing CLI tools with environment setup and process spawning. Each adapter is about 50-100 lines of Go that knows how to launch a specific agent:
type AiderAdapter struct {
execPath string
}
func (a *AiderAdapter) Spawn(ctx *SessionContext) error {
cmd := exec.Command(a.execPath,
"--yes", // auto-confirm
"--message", ctx.InitialPrompt)
cmd.Dir = ctx.WorktreePath
cmd.Env = append(os.Environ(),
fmt.Sprintf("OPENAI_API_KEY=%s", ctx.APIKey),
"AIDER_NO_PRETTY=1", // disable TUI for parsing
)
// Attach to tmux session's stdin/stdout
return a.attachToTmux(cmd, ctx.SessionID)
}
This means you can run any terminal-based agent without the orchestrator needing to understand its internals. The 23 supported adapters range from Claude Code to Codex CLI to custom shell scripts—anything that reads stdin and writes to stdout works.
The closed-loop feedback system is where this gets genuinely autonomous. The orchestrator polls GitHub and GitLab APIs for CI failures and PR review comments, then routes them back to the responsible agent by injecting text into its tmux session's input buffer:
func (s *Session) InjectFeedback(message string) error {
// Escape special characters for tmux
escaped := tmuxEscape(message)
cmd := exec.Command("tmux", "send-keys",
"-t", s.ID,
escaped,
"Enter")
return cmd.Run()
}
When CI fails on an agent's PR, the orchestrator literally types the error message into the agent's terminal as if you pasted it there. The agent processes it like any other input and can autonomously fix the issue. This creates a feedback loop: agent pushes code → CI runs → failure message routed back → agent fixes issue → repeat until green.
The state management uses SQLite with a custom change-data-capture layer. When sessions update (agent outputs text, git state changes, CI status updates), the daemon writes to SQLite and broadcasts the delta to connected clients over WebSocket. The Electron frontend subscribes to these broadcasts and renders real-time terminal output, git diffs, and session status without polling.
The verdict from the architecture is clear: this isn't trying to be a sophisticated multi-agent reasoning framework. It's a process supervisor that treats agents like dumb command-line tools, which is exactly what they are. The sophistication comes from the infrastructure—worktree isolation, terminal lifecycle management, and feedback routing—not from agent coordination primitives.
Gotcha
The fundamental limitation is that this is single-machine, single-user infrastructure. The SQLite backend and local worktree model mean you can't share agent sessions across a team or deploy this to cloud infrastructure. If you want remote execution or multi-user coordination, you're rewriting substantial portions of the architecture.
The terminal-based agent model is also inherently brittle. Agent Orchestrator scrapes terminal output to infer session state, which breaks when agents use TUIs, progress bars, or interactive prompts not designed for machine consumption. The input injection via tmux send-keys works for text, but if an agent expects arrow key navigation or mouse input, you're stuck. The adapter abstraction is one-way: the orchestrator can send input to agents but agents can't call back to the orchestrator or request services beyond what their CLI naturally supports. This rules out sophisticated coordination patterns like agents requesting help from each other or consensus-based decision making. Additionally, API polling for CI and PR state introduces latency—at scale with 10+ parallel sessions, you'll hit GitHub rate limits and degrade the feedback loop that makes autonomous iteration valuable.
Verdict
Use if: You're already running multiple terminal-based coding agents (Aider, Claude Code, Cursor CLI) and waste time manually managing git branches and terminal windows; you regularly have 3+ parallel coding tasks (bug fixes, feature variants, refactors) that would benefit from simultaneous execution; you need a unified control plane across different agent tools without rewriting them to a common framework; or you want autonomous CI fix loops where agents respond to failures without manual intervention. Skip if: You're happy with single-agent, sequential workflows and don't feel branch-juggling pain; you need multi-user collaboration or remote execution on cloud infrastructure; you want sophisticated multi-agent coordination with explicit communication protocols and role hierarchies; you prefer API-based agents over terminal CLIs; or you're building agents from scratch rather than orchestrating existing tools—frameworks like AutoGen or CrewAI are better for ground-up agent development.