Skip to content

Lanes

A lane is one task being built: its own git worktree, its own branch, two agent sessions, and a draft pull request at the end.

flowchart LR
    subgraph main["main checkout"]
        T[("delivery/tasks/*.md")]
        L["launch-lanes.sh"]
    end

    subgraph remote["origin"]
        C[("refs/heads/task/*<br/>claim locks")]
    end

    subgraph lanes["lanes, at most 3"]
        W1["worktree<br/>task/a-001"]
        W2["worktree<br/>task/a-002"]
        W3["worktree<br/>task/a-003"]
    end

    T --> L
    L -->|create-only push| C
    L -->|on a won claim| W1 & W2 & W3
    W1 & W2 & W3 -->|implement, refine, draft PR| PR([review])

Claims

A claim is a create-only push of task/<id>: the first push wins, the loser is told who holds it and picks another task. Nothing stores status; it is derived from git refs and pull request state on every read.

The lane cap is three because three is roughly what one person can review, not what the machine can run.

Two sessions per lane

A lane is two sessions in its worktree: etabli-implement writes the change, then etabli-refine reviews and tidies it on a stronger model. The launcher then commits, pushes and opens the draft pull request. The lane's branch pushes to itself, so a plain git push from the worktree reaches task/<id> and never the default branch.

Where worktrees go

Each lane's worktree goes in ../lanes/<repository>/<task-id>, beside the checkout the launcher runs from, where <repository> is that checkout's directory name. The extra level matters because task ids repeat from one project to the next: two repositories cloned side by side would otherwise both put their a-001 in ../lanes/a-001. Nothing under lanes/ is tracked by git: each directory is a git worktree registered with its own clone, holding that clone's task branch.

To put lanes somewhere else, set DELIVERY_WORKTREE_ROOT to a directory. It replaces the whole default: task ids go directly under it, with no repository level added, so give each project its own directory and never share one between two projects. Where lanes live is each developer's choice rather than the project's, so set it in your own environment for the planning repository, not in a committed file. With mise, that is the untracked mise.local.toml at the planning repository's root:

[env]
DELIVERY_WORKTREE_ROOT = "{{ env.HOME }}/lanes/shop"

Every launch then uses it: a-001 runs in ~/lanes/shop/a-001, whether you start the launcher yourself, the follow loop starts it, or the orchestrator does. direnv or an export in your shell work the same way. For a single launch, --worktree-root <dir> takes precedence over the variable.

Parallel or serial

Not every task set parallelizes. Two tasks editing the same file cannot be merged independently however independent they look, and on an existing codebase work converges on existing files far more often than new work does. Expect a serial set there; it is the normal case, not a degraded one.

Decomposition finds this out and records it in the task files, so nobody has to remember it at launch time:

  • conflicts_with names the tasks one task must never run beside.
  • parallel: false makes a task run alone: it starts only when nothing else of the set is in flight, on anyone's machine, and nothing starts while it runs. Use it for a lockfile change, a migration, or a rename across the repository, where the shared ground is too wide to list.
  • A set that serializes marks every task parallel: false, and says Run as: serially in delivery/decomposition.md; the validator refuses the two disagreeing.

The launcher and the "what's next" answer both read those fields, so a serial set runs one lane at a time with no flag to pass. Run the launcher again after each merge to start the next task.

A serial set still goes through every phase and every gate: the requirements, contracts and task files are what stop the work encoding guesses, whether or not it runs concurrently. What changes is who needs the lanes. When one person holds the whole set, skip the launcher and invoke etabli-implement, then etabli-refine, per task in dependency order; the claim only earns its keep when several people pick from the same tasks.