COORD: 44.21.90
OFFSET: +12.5°
SYS.READY
BUFFER: 99%
FOCAL_PT
BACK TO DEVLOG
EXP

Seven Backlog Items, One Session: exp Forking Itself

I cleared a 7-item backlog in one session by using exp's own APFS clonefile forking to run three parallel Opus agents — zero merge conflicts. The move that made it work happened before any agent launched.

2026-02-25 // RAW LEARNING CAPTURE
PROJECTEXP

Seven items had piled up in BACKLOG.md — a mix of bugs, perf issues, and missing features in exp, my APFS-clone branching tool. Slow exp ls, unbounded column widths, stale-fork triage, a trash command that reported the wrong size and couldn't trash the fork you were standing in. The obvious play was to grind through them serially. Instead I used exp to fork itself three ways and ran an Opus agent in each clone simultaneously. The interesting part isn't the parallelism — it's the one piece of prep work that decided whether parallelism would produce clean diffs or a merge nightmare.

Clustering by file ownership

Parallel agents only stay out of each other's way if their edits don't overlap. So I clustered the seven items into three streams by which file they touch:

  • Stream A (ls.ts): perf, column caps, stale-fork color coding
  • Stream B (trash.ts): self-trash, size fix, elapsed time
  • Stream C (new.ts): auto-gitignore .exp in forks

That mapping is the whole safety model. If two agents edit the same file, you get conflicts no matter how good the specs are.

The shared-util problem

Pre-flight surfaced the catch: streams A and B both needed getDivergedSize(), which lived in ls.ts. Two agents editing ls.ts is exactly the conflict the ownership map was supposed to prevent. So before forking anything, the orchestrator (me, in the main checkout) extracted the shared code into new modules:

  • src/core/divergence.tsgetDivergedSize, formatBytes, getGitStatus, and friends
  • src/utils/format.tsfmt(), previously inlined in new.ts

With the shared dependency pulled out and committed, the three streams became genuinely disjoint. Then the fork:

dist/exp new "stream-a-ls-improvements" --no-terminal --json
dist/exp new "stream-b-trash-fixes"     --no-terminal --json
dist/exp new "stream-c-gitignore"       --no-terminal --json

Three APFS clones, roughly a second each.

Fan out, then reconcile

I launched three Task agents at once, all model: opus, each with a tight spec — drop the getDivergedSize() call from the compact list path (the 2s bottleneck), add truncate() caps, color the status dots green/yellow/red, swap du -sh for diverged size in trash, append .exp to each fork's .gitignore. They finished independently:

StreamTimeTool usesTests
A (ls rewrite)273s37141 pass
B (trash fixes)99s21112 pass
C (gitignore)72s15112 pass

Spec complexity tracks runtime almost linearly: the simple gitignore change took a quarter of the time the ls rewrite did. Reconciliation was the payoff for the upfront work — file overlap was zero, so merging was literally copying changed files out of each clone with /bin/cp. No hand-merging. Then the verification gate:

pnpm run typecheck  → clean
bun test            → 141 pass, 0 fail
pnpm run lint       → no fixes applied

Built the binary, ran dist/exp ls — instant now, color dots, capped columns. Trashing the three forks demoed the new elapsed-time feature back at me: 6.9s, 6.1s, 6.4s. Tagged v0.6.0 and pushed.

The durable move

Extract shared code before you fork, not after agents collide on it. Parallel orchestration lives or dies on the file-ownership map being real — and a map is only real once nothing two streams need still lives in one file. The orchestrator's job isn't to fan out fast; it's to do the boring extraction that makes fanning out safe. Get that right and reconciliation stops being a merge and becomes a copy.

One incidental lesson from generating the release image: ask an image model for a "fork" and it draws a kitchen utensil. The negative prompt that fixed it — fork utensil, kitchen, food — is the kind of thing you only learn by getting silverware back the first time.

LOG.ENTRY_END
ref:exp
RAW