Before writing this, I glanced at the build machine: 5 tmux sessions running, 372 task directories under ~/.local/state/ai-jobs totaling 3.1 GB, load average 2.90 and drifting between two and seven the whole time. The machine has 128 cores, up for four and a half days. Those numbers are a single sampled instant—they’d changed again by the time I finished this paragraph, which is itself part of the point.
The projects on hand at the same moment: a Rust + SQL site reworking its statistics query, a Next.js app retiring its service worker, a Bun + Hono service fixing a tunnel origin, a Prisma + tRPC monorepo, a C# game private server filling in data fields, a C/C++ SDL3/WASM port, and two Zola/Hexo documentation sites. 64 directories and 36 git repositories under ~/Code.
This isn’t the “I have a lot of windows open” kind of parallelism. Anyone can open windows; the hard part is making progress on all of them at once without contaminating each other.
First, why it’s even worth it
With a single project, the most expensive part of AI collaboration is waiting: ten minutes for a full test run, five for a build, however long for browser regression. During that time the only things a person can do are sit there or switch to something else—and switching back means reloading context.
The first payoff of running eight projects in parallel is exactly this: the waiting gets filled by other lines. A’s build is running while B’s failure log just comes back; C waits on a human confirmation while D’s tests go red and need fixing. The machine’s throughput gets pulled up, and the human brain no longer has idle gaps.
The second payoff is reusing a solution from one line on another. Today, figuring out in av-site that “creating an index must stay inside the same transaction block as acquiring the lock” naturally led to thinking about “the deletion order when retiring a worker” in AstroDX—both are cases of “nothing can slip in between the two steps.” That kind of cross-project intuition only happens when you have several lines in hand at once.
But the payoff has preconditions, and here they are.
Rules must be layered, not copied
~/.agents/AGENTS.md is the single source of truth; ~/.codex/AGENTS.md and ~/.dsh/AGENTS.md are symlinks to it, and ~/.claude/CLAUDE.md references it via @. To change a rule you only change that one copy—three copies will inevitably drift.
The source of truth holds only what’s universal across projects: hold conversations in Chinese, Conventional Commits, the local machine isn’t the build machine, which machine to go to remotely, how to persist tasks over 30 seconds, where the proxy is.
Project-level AGENTS.md files only describe what’s unique to that project, and some really do write them narrowly:
av-site/AGENTS.md: all five CI steps green is the prerequisite for merging; changing OpenAPI types requiresbun run gen:client; how to spin up a temporary postgres for database tests with#[ignore]; curl from the build machine needs a UA or Cloudflare returns 403; addSET default_transaction_read_only = on;before production troubleshooting.eagler-touhou-next/AGENTS.md: this Mac is read/write and Git only; which checkout directory on the build machine; bun is pinned to 1.4.2; where the project’s own postgres:18 container lives.th15/AGENTS.mdis only 15 lines: the ownership of two trees.
That middle category—“which machine is the build machine”—is actually written in both files. It looks redundant, but the division of labor differs: the source of truth says “there is a build machine, pick by priority,” while the project file says “this project is fixed to this one, the checkout directory is here.” The former is a rule, the latter a fact.
There’s one more hard rule: every project must be able to state clearly “where it runs”—if you can’t write it down, the project isn’t ready to be parallelized yet.
Tasks must have an identity
Something run remotely can’t just be a ssh host 'bun run build' and done with it—drop the SSH and it’s gone, and reconnecting doesn’t tell you where it got to.
The rule in the source of truth, “remote SSH task persistence and reconnection recovery,” specifies this (shared by all projects):
Each task gets an independent ID, an independent tmux session, and an independent ~/.local/state/ai-jobs/<任务ID>/ directory. Inside the directory, run.sh begins with set -uo pipefail, umask 077, exec > output.log, trap writing the exit code into a file, echo $$ > pid, date -Is > started, then explicitly exporting PATH and HTTP(S)_PROXY.
Startup, logs, exit code, process ID, and start time are all five on disk. If SSH drops, go back by the original ID and read the status—do not resubmit. If the tmux session is gone but there’s also no exit code, the status counts as unknown: keep cross-checking the process and artifacts, and don’t treat it as success or as failure.
What’s forbidden is sharing a fixed ai-task session—several lines firing send-keys into the same session amounts to throwing commands into a shell you don’t recognize.
Recent tasks’ exit codes include both 0 and 1 (adjust-download-policy-red is 1, adjust-prod-cold is 1, a batch of adjust-consent mixed 1/0). Parallelism isn’t eight lines all green—it’s eight lines’ results each landing on disk, each queryable, with the red ones not dragging the others down.
Write to one place at a time
Two lines editing the same file at once is the number-one incident source in this workflow.
The unit of dispatch isn’t the project, it’s the file. Today the write-sets of the four lines are: av-site/infra/migrations/*, AstroDX/apps/web/public + src/lib, acgn-flow/src/server/*, drama-site/apps/web/src/*. The four groups are mutually disjoint.
(This is part 1 of 2 of the same post.)
Conversely, if the same package.json or root tsconfig needs to be changed, make them sequentially dependent: finish one before starting the next. Hard parallelism here only produces git conflicts and edits that overwrite each other.
When shared configuration must be changed across projects, I’d rather pause the line I’m on and wait for it to land.
Verification is tiered by scope of change
What most easily slackens under parallelism is verification, because every line wants to wrap up quickly.
The rule is tier by change, not by how much time is free:
- One file changed → lint just that file, run just that test file; it can be done locally.
- Cross-module change → go up to the build machine and run the whole-repo check for that project (AstroDX’s
scripts/remote/verify.shruns 739 tests + lints 719 files). - Anything with side effects (migrations, deployments, data fixes) → add real-device acceptance; for example,
av-sitechanges to playback must runcheck-live-playback.pywith a rate-limit round, and all-green smoke isn’t enough.
The only valid reason to downgrade verification is “this change isn’t worth that cost,” never “I’m busy right now.” Being busy is precisely what parallelism creates; trading busyness for cheap verification just pays back the time parallelism saved.
The costs, recorded in plain sight
The above is method; below is the ledger.
Context dilutes itself. With eight lines in hand, the memory of each grows thin. So the moment a line lands, “why it was changed this way” must go straight into a code comment or that project’s docs—don’t count on remembering next time. Of the 86 lines in 0055's migration file, 52 are comments, and AstroDX’s docs/verification/service-worker-retirement-2026-10-30.md even records when the script itself went wrong—both for this reason.
Review is the bottleneck, and it doesn’t scale with parallelism. Eight lines each produce output, and the merge decision all lands on one person. So better to let the AI go slower and produce less than to have it change 20 files at once—a giant change that can’t be decomposed takes longer to read through than to do yourself.
Build-machine resources are shared. A load average of two to seven looks low, but that’s headroom on 128 cores; at the same moment, two or three of several tmux sessions are running browser regressions and competing for memory. Lines running full test suites need to be scheduled on idle cores; single-core cases like taskset -c 0 get slowed by other lines, and timing issues on a slow CPU only surface under that condition—which is both a blessing and a trap.
Task directories grow if not cleaned. 372 directories, 3.1 GB, four and a half days of uptime. After a run, --keep keeps the evidence, and it should be swept a week later; it hasn’t been swept yet, which already counts as debt.
One more thing often overlooked: parallelism makes “done” cheap. One line goes red and nobody cares; another goes green and keeps moving, and the red one gets buried. So at the end of each day, look through the directories with non-zero exit codes, not at “how many were delivered today.”
Who this suits
Who it doesn’t suit: people whose changes are strongly linear, who can only think about one thing at a time, who dislike writing rule files.
There’s only one prerequisite for it to suit you: the project must already be verifiable in a remote form. A build machine, a verify script, and “where to run it” written into files—with all three in place you can split lines; missing any one, and raising parallelism only moves “stuck” from local to remote.
As for the AI’s side, it neither knows nor cares that other lines are running. It’s kept apart by the “don’t touch that directory” constraints in rule files, by the identity of task directories, by disjoint write sets. All these partitions are built in advance by a human; the AI just runs inside them.
When eight lines run at once, what I’m actually doing is quite boring: reading exit codes, watching logs, judging which line should continue and which should stop, writing the reasons into files. The real parallelism happens on disk and CPU; the human part remains serial—except what’s serialized is judgment, not typing.
(This is part 2 of 2 of the same post.)
