Worktrees for parallel coding agents
Give each coding agent its own lane: a reusable worktree that keeps its dependencies and build caches, held with a lease so no other session takes it, with shared things (the emulator, heavy builds, ports) handed out one at a time, and a check that the lanes still pass together before and after each merge.
$ lane new dfk feat/core --sparse packages/core --link-deps fetching dfk... lane dfk-2 (new slot): branch feat/core from origin/develop, held for feat/core, lease eb9fc0 sparse: packages/core (and the files at the root) dependencies: linked from dfk-1 (45216 files, hard links) next: cd F:/demo/wt/dfk-2 && claude (agents: lane brief dfk-2; when pushed: lane done --lease eb9fc0)
.git; each has its own files, dependencies and build output. The state is
git worktree list itself: no daemon, no database. Open the diagram
git worktree gives each task its own folder. The hard parts come after: installing again in every new folder, two
agents on one emulator or one port, four builds at once running out of memory, and two branches that merge cleanly as text but
break each other.
Numbered worktrees <repo>-<n> that come back to a pool. A task gets the slot it had before, else a
free one; dependencies are installed again only when a lockfile changed, so a new task starts in seconds. In a big
repository a slot can hold only the folders its task needs (--sparse), and npm dependencies can come as
hard links from a lane on the same lockfile (--link-deps).
A slot is held with git worktree lock and read back with this hold's lease, so two sessions never take the same
slot. lane done refuses to give back work that is not on the remote.
lane claim emulator waits for the Android emulator, lane with build -- <cmd> lets two heavy
builds run at once, and lane port 3000 gives each slot its own range of ports.
lane verify merges every open lane on top of the base and runs the checks before the first one merges.
lane sync rebases the rest after a merge and checks them again.
lane clean gives back the lanes whose work merged, squash merges included, even without asking GitHub. A lane
still held for follow-up rounds stays.
Each lane gets a CLAUDE.local.md with these rules, and lane brief prints them for a sub-agent. Heavy
git hooks such as pre-push wait for a build slot.
Two agents on one repository, from the first command to the slots given back.
$ lane new myrepo feat/a # slot myrepo-1, branch feat/a $ lane new myrepo feat/b # slot myrepo-2, branch feat/b $ lane list # held or free, branch, changes, PR and CI $ lane verify myrepo # before merging: do the two pass together? # merge feat/a's pull request, then: $ lane sync myrepo --push # feat/b onto the new base, checked again, pushed $ lane clean --yes # merged slots back to the pool, main clone pulled
With junto each lane keeps its own task and quality gates, and
/junto:status all shows every lane's task in one table (or a board with /junto:status html). junto warns
when the base moved since a lane was verified, the same lesson lane sync acts on.
lane 0.10.1 on a copy of dynamic-field-kit (1,145 npm packages): two tasks, the second sparse with its dependencies linked from the first. 37 seconds of real time; every line is what the commands printed.
Press Play to watch the round.
$ lane new dfk feat/formsfetching dfk...lane dfk-1 (new slot): branch feat/forms from origin/develop, held for feat/forms, lease c9a58edependencies: no other lane installed from this lockfile yet> npm ci --no-audit --no-fundnpm warn deprecated @ngtools/webpack@22.2.5: Angular's Webpack support is deprecated. Use the esbuild and Vite-based "@angular/build" package instead.npm warn deprecated @angular-devkit/build-angular@22.2.5: Angular's Webpack support is deprecated. Use the esbuild and Vite-based "@angular/build" package instead.> prepare> husky && git config commit.template .git-commit-template.txt || trueadded 1145 packages in 14snpm warn install-scripts 5 packages have install scripts not yet covered by allowScripts:npm warn install-scripts @parcel/watcher@2.6.0 (install: node scripts/build-from-source.js)npm warn install-scripts esbuild@0.28.2 (postinstall: node install.js)npm warn install-scripts lmdb@3.5.6 (install: node-gyp-build-optional-packages)npm warn install-scripts msgpackr-extract@3.0.4 (install: node-gyp-build-optional-packages)npm warn install-scripts unrs-resolver@1.12.2 (postinstall: node postinstall.js)npm warn install-scriptsnpm warn install-scripts Run `npm install-scripts ls` to review, or `npm install-scripts approve <pkg>` to allow.hooks: pre-push wait for a build slot in this lanenext: cd F:/demo/wt/dfk-1 && claude (agents: lane brief dfk-1; when pushed: lane done --lease c9a58e)$ lane new dfk feat/core --sparse packages/core --link-depsfetching dfk...lane dfk-2 (new slot): branch feat/core from origin/develop, held for feat/core, lease eb9fc0sparse: packages/core (and the files at the root)dependencies: linked from dfk-1 (45216 files, hard links)next: cd F:/demo/wt/dfk-2 && claude (agents: lane brief dfk-2; when pushed: lane done --lease eb9fc0)# the two agents work and push$ lane list --locallane state branch local PR / CIdfk-1 held: feat/forms, 1 min feat/forms 0 changed, 0 unpushed -dfk-2 held: feat/core, 0 min feat/core (sparse: packages/core) 0 changed, 0 unpushed -$ lane verify dfkfetching dfk...verifying in dfk-3lane branch on origin/developdfk-1 feat/forms mergeddfk-2 feat/core mergeddependencies: linked from dfk-1 (45276 files, hard links, 4 node_modules folders)> npx vitest run --root packages/core (in dfk-3) RUN v5.0.5 F:/demo/wt/dfk-3/packages/core Test Files 26 passed (26) Tests 395 passed (395) Start at 22:33:32 Duration 1.67s (transform 52%, import 20%, environment 17%, tests 7%, worker 4%)2 lane(s) together on origin/develop: checks passgave back dfk-3 (was: verify)# you merge the pull request of feat/forms$ lane sync dfk --pushfetching dfk...> npx vitest run --root packages/core (in dfk-2) RUN v5.0.5 F:/demo/wt/dfk-2/packages/core Test Files 26 passed (26) Tests 395 passed (395) Start at 22:33:37 Duration 1.56s (transform 53%, import 20%, environment 16%, tests 8%, worker 3%)lane onto origin/developdfk-1 skipped: work already in origin/develop (lane clean gives it back)dfk-2 rebased, checks pass on its folders only (lane verify checks the whole), pushed$ lane clean --yes --forcefetching dfk...give back dfk-1 (no PR, work already in origin/develop)keep dfk-2 (no PR)gave back dfk-1 (was: feat/forms); local branch feat/forms deleted (its work is on the remote or merged)dfk: main clone pulled to 6d713a5 feat(react): validate on blur$ lane list --locallane state branch local PR / CIdfk-1 free (last: feat/forms) (detached) 0 changed -dfk-2 held: feat/core, 1 min feat/core (sparse: packages/core) 0 changed, 0 unpushed -dfk-3 free (last: verify) (detached) 0 changed -
Your turn: you merge the pull request of feat/forms.
A recording, not a live system. The origin was a local copy, so "you merge" was a push of feat/forms into develop there. Recorded with replay/record.sh; the raw output is replay/lane.jsonl.
lane new <repo> <task> |
Take a slot for a task: new branch, submodules, env files, install when lockfiles changed, notes for the agent.
--pr <n> works on a pull request's branch; --sparse <folder> checks out only those
folders; --pool review keeps reviews in slots of their own.
|
|---|---|
lane done [<lane>] |
Give the slot back once its work is pushed or merged. |
lane list |
Every lane: held or free, branch, local changes, pull request and CI. |
lane verify <repo> · lane sync <repo> |
All open lanes merged together and checked; after a merge, the rest rebased and checked again. |
lane clean · lane free · lane rm |
Give back merged lanes, free a hold left behind by a closed session, delete a slot for good. |
lane claim · lane with · lane port |
The emulator for one lane at a time, a cap on heavy builds, a range of ports per slot. |
lane brief |
The rules for an agent working in a lane, to paste into its task. |
Built and used daily on Windows; CI runs it on Windows, Ubuntu and macOS.
gh, logged in, for pull request and CI state (optional).
Node does not run .ts files inside node_modules, so lane is not on npm.
$ git clone https://github.com/vannt-dev/lane $ cd lane && npm install && npm link
LANE_REPOS holds your clones, LANE_ROOT the slots. Both are required.
# Windows $ setx LANE_REPOS D:/code $ setx LANE_ROOT D:/wt # macOS / Linux (shell profile) export LANE_REPOS="$HOME/code" export LANE_ROOT="$HOME/wt"
lane new --open starts Claude Code in a new Windows Terminal tab on Windows, in a new tmux window inside tmux; elsewhere it prints the command to run.
lane done detaches the slot.lane verify catches lanes that break each other only as far as your checks reach: tests, typecheck, lint.