Too many agents, one Mac
Coding agents make it possible to do lots of work in parallel. I always have several (often many tens of) agents working at once, especially when working in Saggar. They might be working across many repositories, or in the same codebase. They write code, run the tests, and commit... then BAM!
Three concurrent agents try to open PRs for their work at around the same time, triggering pre-push validation hooks, and my computer almost freezes.
Pre-commit and pre-push hooks make rigorous validation non-optional: an agent finishes its work, runs the tests, then commits or pushes, and the hook runs much of the same validation again. For a big Swift codebase, a test suite or build can be intensive enough that I have to be careful what else the machine is doing. Multiply that by several agents and worktrees starting together, and a quiet afternoon of delegation turns into fans, swap, and a frozen laptop. Whether it's pre-commit and pre-push hooks or normal agent work, this is a constant risk.
But the agents aren't messing up. Each behaves sensibly in isolation, exactly as I want it to. It's only together that they can start 3 builds, 4 test suites, and a browser run on the same machine without knowing that the others exist.
I'm not the first to encounter this issue - in December, Android developer Matt McKenna described the same failure mode: "The agents don't know about each other." When two of his agents ran Gradle together, a build that normally took 3 minutes stretched to 15. CPU contention slowed every job, memory pressure was worse, compression and swap kept short jobs alive for longer, and longer jobs overlapped more, adding more pressure.
More parallel work produced less completed work.
Git worktrees suit agents: each gets its own checkout and can work on a separate branch without trampling another. Worktrees only isolate the code. The source trees are separate, but the CPU and memory aren't. Build output is often separate too, so each worktree may compile the same dependencies and hold its own caches. The hooks attached to the repository don't know that another worktree is already running the same check. Each agent sees a simple local decision: the instructions say to run the tests, so run the tests. No agent can see that the machine is already at its limit.
You could try:
- Running fewer agents... but I gotta go fast.
- Using lock files... but they need project-specific setup and don't coordinate work across repositories.
- Making agents aware of one another... but coordination consumes tokens and gives the agents another thing to get wrong.
- Having agents queue their own work... but they can bypass the queue, and Git hooks don't know it exists.
- Working in the cloud more... but that leaves a perfectly good machine idle, and I need to see and use the build output locally.
A queue makes overload explicit. When the machine can't safely absorb another expensive job, that job waits instead of making every running job slower. But serializing everything would waste useful capacity, so the scheduler needs to admit as much work as the machine can handle, protect memory, and choose sensibly when several jobs are waiting. Plus the impact is global to your device, so the queue needs to be global - the scheduler must live at the same level as the constrained resource, not at the level of whichever agent happens to request it.
McKenna put his FIFO queue behind an MCP tool because his agents timed out while waiting in the shell. That solved his client problem, but an agent still has to know about the tool and choose it. Git hooks and human commands don't. I also haven't experienced the same timeout constraint (as long as your queue is transparent about its stateand that its holding the job).
McKenna solved the waiting problem for his clients. Turnstile solves the coordination problem beneath every client.
The agent runs swift test, the Git hook runs swift test, and I run swift test; the same machine-wide gate must see all three.
A machine-wide queue is necessary, but FIFO isn't enough. Memory is a moving target, foreground work deserves priority, and the scheduler should preserve safe parallelism rather than reduce the machine to one job at a time.
I built Turnstile as a machine-wide, memory-aware gate for builds and tests on macOS.
Turnstile puts lightweight shims for tools such as swift, npm, vitest, and playwright at the front of PATH.
Normal commands pass straight through.
Heavy builds and tests ask a small daemon for permission to start.
The coordination happens beneath the agents.
Claude Code, Codex, a Git hook, and a command typed into the terminal all pass through the same gate.
No caller needs to know how busy the machine is.
It queues excess work, allows safe parallelism, protects memory, and resolves contention without spending agent attention on any of it.
Turnstile is a priority- and resource-aware admission controller.
FIFO only breaks ties after bumps and human priority.
Turnstile enforces separate capacity for compile, test, and browser jobs.
It learns peak memory use by command and project, then admits a job only when there is room.
Foreground commands take priority over background agent work.
It conservatively backfills around blocked jobs.
When 2 agents request the same command against identical working-tree contents, Turnstile runs it once and gives both callers the output and exit code.
Queued runs can supersede obsolete work.
Different worktrees with different changes still run separately, as they should.
Git hooks need no special case: a pre-push hook that calls npm test or swift test reaches the same shim, then waits, starts, or joins an identical run according to the state of the whole machine.
A job above its learned ceiling becomes a runaway candidate. Turnstile warns, pauses, or kills it only when memory pressure warrants.
Memory is a moving target. Admission control handles jobs whose cost is predictable. Real builds aren't that polite. Turnstile watches macOS memory pressure and swap growth while jobs run. Under memory pressure, it reduces the parallelism passed to supported tools. When swap grows, it stops admitting new work and can pause the newest agent job until memory recovers. At least one job keeps running, so the queue still drains. Apple calculates memory pressure from free memory, swap rate, wired memory, and cached files, so free memory alone is a poor guide. Compression and paging can make the headline number look tolerable while the machine is busy writing itself into treacle. Swap growth is the useful signal.
In fairness, Turnstile adds another layer between a command and its result. If its daemon breaks, Turnstile fails open; it can also be disabled globally. It schedules commands without becoming a new reason they fail.
Turnstile stops expensive work from arriving at once without making builds cheaper or reducing the disk cost of worktrees. Agents now wait before some builds. That looks slower in one terminal, but the queue is honest: it says what is running, why a job is waiting, and roughly when it can start. Across the machine, more jobs finish, the foreground stays usable, and pre-commit and pre-push hooks no longer turn a routine commit into an accidental load test. Turnstile is open source and installs through Homebrew. The defaults are designed to work without per-project setup.
Parallelism isn't the same as throughput. Once software can create work faster than the machine can absorb it, delegation needs traffic control.
