ADAM LIN

SESSION · LOG

LOG

← LOG

· LOG ENTRY

Running coding agents from the pit wall

How I run Claude Code and Codex in parallel through cmux and a small launcher: one worktree, branch, and port pair per agent, Fable first with a GPT-5.6 Sol fallback, and a landing step that keeps me in the loop.

You know that moment when you have three coding agents going at once and you lose track of which terminal is which? One is stuck on a permission prompt you haven't seen. Another just started a dev server on the port the first one is testing against. A third made a nice change on the branch the second one is still editing. Twenty minutes later you're untangling git instead of reading the work. I ran into all of that, and it's what pushed me to build the setup I use now.

I call it the pit wall. It's a small directory that sits next to my two repositories, this website and vd-macan, the public data and analysis repo for the Macan project. Inside are a handful of shell scripts, a rulebook, and a few short guides. The agents are Claude Code and Codex. They run inside cmux, a macOS terminal with a sidebar of named workspaces. Here's how it fits together, and where I stay in the loop on purpose.

One command per task

Every piece of work starts the same way. I describe the task in a sentence and hand it to launch:

launch --background "tighten the /log index page spacing"

That one line does the provisioning I'd otherwise do by hand, and it does it under a lock, so two launches can't collide.

The task text becomes a slug: the launch date, then up to forty characters of the words. Slugs are never reused, even after the work lands. The launcher creates a git worktree at <repo>/work/<slug> on a fresh branch, agent/<slug>, cut from main. The work/ folder is gitignored, so an agent never touches the working tree of main. Then it reserves a port pair: the first free even-odd pair from 4400 up, checked against every other reservation and every live listener, and never 3000 or 4356, the two defaults my own checkout uses. The dev server gets the even port and the test suite gets the odd one. Website worktrees also get their own node_modules, an APFS copy-on-write clone of the main install, so an agent can add a package without touching mine. All of it is written into a TASK.md at the worktree root, and that file goes on git's local exclude list so nobody commits it.

I don't have to say which repo. The launcher lowercases the task and looks for signals. Words like /log, home page, content/log, layout, page, or post point at the website. Words like raw data, session data, tools/, imu_sd, or day1_analysis.py point at vd-macan. If the task hits one family, that repo gets the worktree. If it hits both, says "both repos" or "cross-repo" outright, or hits nothing the launcher recognizes, each repo gets a worktree on the same branch name, and each TASK.md names the other. Paired is the safe fallback. It costs a little disk and never loses work. When I already know where the task belongs, I say so up front, launch website "..." or launch vd-macan "...", and inference is skipped.

What the agent reads first

TASK.md is short. It holds the task, and it points at the files every agent reads before touching anything: the rulebook, an orchestration guide, a working guide for the target repo, and two engineering playbooks, one on small surgical diffs and one on test-first work. It lists the worktree path, the ports, and the main checkouts as read-only. It ends with the finishing rules: commit to your branch, no AI co-author trailer, don't merge, don't push, report what you did. The prompt the launcher hands the agent is one sentence: read TASK.md, read every rulebook and guide it names in full, then do the task.

The rulebook is the part that matters. It's one file, RULES.md, and it holds everything durable: which numbers on this site are owner-verified, the writing voice, the ban on em dashes, the tests-are-the-contract rule, how worktrees and ports work, and what agents may never do. Both repos carry a thin CLAUDE.md that points at it, so even a session I open by hand inside a repo inherits the same rules. When I change my mind, I change the file, and the next agent reads the new version. Nothing durable lives in one agent's memory where I'd have to remember to repeat it.

Where the agents live

Inside cmux, launch --background creates a workspace named after the slug, with the worktree as its working directory and the agent starting in it. It's created unfocused, so it shows up in the sidebar without pulling me away from what I'm doing. Six tasks means six named workspaces down the sidebar, each an agent in its own worktree on its own ports. I click into one when I want to watch or answer a question, and click out. Outside cmux, the same flag uses Claude Code's own background mode instead. And plain launch, with no flag, runs the agent right there in my terminal.

Because every agent is isolated, parallel is the default. Independent tasks go out at the same time. The rulebook tells agents the same thing about their own subagents: split independent work, give each worker its own files, and coordinate through main, never through each other's trees.

Fable first, then Sol

By default every launch starts on Claude Code running Fable at xhigh effort. If Claude can't start (the binary is missing, or it exits with a status the launcher treats as a failed start), the launcher runs the same brief through Codex running GPT-5.6 Sol, also at xhigh. Same TASK.md, same rulebook, same worktree, so a change of model doesn't change the rules.

The cmux case is more careful, because a usage-limit screen inside a live session isn't an exit code. For a bounded window after launch, about thirty seconds by default, a small watcher reads the agent's own terminal surface, only that one. If it sees a recognized usage-limit line, it records the reason and, when the message shows one, the reset time. It sends an interrupt to the exact Claude process it captured at startup, a few times at most, confirms that process has stopped, and starts Codex in the same workspace. It never types into the terminal, so a later foreground process can't receive a stray keystroke. Later launches skip Fable until the reset, or for a six-hour cooldown if the message couldn't be parsed. After that, the next launch tries Fable again, and when it comes up clean the router marks it available; a generation counter keeps a stale success from clearing a newer cooldown. The launcher owns the fallback. The agents don't build retry loops of their own.

Watching from the wall

status is the glance. It prints the router's state first, so I always know which model the next launch will get, then one line per worktree per repo: slug, branch, ports, dirty file count, where the agent is running (a cmux workspace, a Claude Code background session and its short id, or a process id), and its route (provider, model, effort, router health). Then it lists anything listening on an assigned port, so a dev server an agent left running for me is right there.

Agents verify their own work before they call it done. For the website that means npm test on the assigned test port, which builds the production bundle, starts the built server, and runs the rendered-HTML checks against real responses; npm run lint; and, for anything browser-visible, the dev server on the assigned dev port left running so I can look. The agent reports the exact localhost URL. That's where the human part starts. I open the page at desktop and phone widths, and I read the diff. cmux can show me the branch diff or just the agent's last turn, and land <slug> with no flags prints the commits and diffstat across both repos.

The tests are the contract, and they are strict for a reason. They fetch the built pages and check the copy itself: the demo-data label appears exactly once, the banned words and phrases don't appear, every route declares its canonical URL, the sitemap lists every published post. If an agent changes canonical copy, it changes the matching assertion in the same commit, and I see both in the diff. The guards are not permission to invent content that passes them.

Landing

Merging is mine. land <slug> --merge merges the agent branch into main wherever it has commits and sweeps an untouched paired worktree. Before it touches anything, it checks that the worktree has no uncommitted changes, that the agent is finished (its cmux workspace closed, its process gone, or its native session in a terminal state), and that no commit carries an AI co-author trailer. A human co-author is fine, and it never rewrites a message. The merge subject is the task I typed at launch, tidied up. Then it removes the cloned node_modules, the worktree, and the branch. It never pushes. If the work is wrong, land <slug> --abandon removes everything and keeps nothing. Pushing main is a separate act, done by me, and Vercel deploys from there.

What stays with me

The pattern underneath all of this is simple. The agents get isolation and a rulebook. I keep the decisions. I write the task. I decide what has to pass. I look at the page and read the diff. I merge. When an agent thinks a rule should change, it says so in its final report, and I make the change in the rulebook myself, so nothing changes quietly. When something ships, I've checked every number on it, and the source and the checks are committed beside the result so someone else could reproduce it.

If you run more than one agent, you don't need my scripts. You need three things: one worktree, branch, and port pair per agent; a rulebook the agents read before they start; and merging kept as a step a person takes. That's the whole trick. Everything else here is those three, made hard to get wrong.

NEXT IN THREAD → Six runs, two damper maps: first data from Storm Stadium

← Back to the log