# Lanes: several agents, one repository Two agents in one checkout share one git index and one HEAD. So one agent's `git add -A` commits the other's files, one agent's `pull` moves files under the other mid-task, and both pick "the next decision number". Lanes remove all three. Decision record: ADR 0031. A **lane** is one agent's (or one task's) own git worktree and branch of the same repository, the paths it owns, and one serialized way onto `main`. ``` ferry lanes new idle-ish game --owns "game/**" --owns "docs/game-*.md" ferry lanes new idle-ish art --owns "art/**" --owns "assets/**" ``` ## Where a lane lives `/.lanes/`, on branch `lane/`, inside the project's own folder. Why inside the repo and not a sibling `.lanes/`: - the repo folder is already the project's housing; a sibling would show up in `repos/` as a second "repository" to every scan and to `ferry root show`; - it is not under `work/`, which holds transient per-order worktrees that `ferry root migrate --prune-stale-worktrees` is allowed to move. A lane is long-lived; - it is **excluded** by `.git/info/exclude` (`/.lanes/`), so `git status` in the main checkout never mentions it and `git add -A` there cannot stage it. `repos/` and `work/` are machine-local and never synced (`syncscope`), so a lane is not synced either. `ferry channel worktree` (per-order, transient, `ferryman--`) is unchanged and uses the same git primitives; lanes are the long-lived, human-named sibling of it. The CLI is `ferry lanes ...` (plural). `ferry lane iroh ...` is the iroh transport lane and is unrelated. ## Commands | command | what it does | | --- | --- | | `ferry lanes new --owns ... [--shared ] [--append-only ] [--base main]` | worktree + branch + hooks + signed record. Safe to repeat. Needs git 2.20+. | | `ferry lanes list [--project p] [--all] [--json]` | owner, owned globs, ahead/behind, dirty, live sessions | | `ferry lanes enter [--session id]` | register this session in the current tree; prints the rules; warns if another live session shares the tree | | `ferry lanes leave` | end the session | | `ferry lanes land [--push] [--no-fetch] [--allow-foreign why] [--wait s]` | rebase + fast-forward, one at a time | | `ferry lanes retire [--force]` | remove worktree and branch, free the globs; refuses to lose work (see Retiring) | | `ferry counter next [--project p] [--start N]` | print the next number | | `ferry counter list / check / policy ` | who holds what; contested numbers; master-signed policy | | `ferry root show` | lists each project's lanes | | `ferry doctor` | warns about shared trees and stale or far-behind lanes | ## Ownership Each lane declares globs it owns (`*` stays in a directory, `**` crosses directories, a bare directory name covers everything under it). The declarations are one **signed record per lane** in the project's channel (`lanes/.json`, written only by the lane's owner, so sync never sees two writers). A record that does not verify is ignored and reported; it grants nothing. A **hook set**, installed per worktree (`extensions.worktreeConfig` + `git config --worktree core.hooksPath`, so no other worktree is touched; needs **git 2.20 or later**, checked up front), covers every standard client hook. Each is a small wrapper that reaches the repository's own hook of the same name: the folder its `core.hooksPath` names (husky's `.husky/_`, say) or else `/hooks`. Ours adds one thing, in `pre-commit`: the ownership check, which runs first. Nothing the repo already runs is dropped. The check classifies every staged path (read with `git diff --cached -z --no-renames`): - owned by this lane: fine; - owned by **another** lane: the commit is **refused**, naming the path and the lane; - owned by nobody: allowed, reported; - a shared file (below): allowed, reported. **When the channel cannot be reached** the check does not guess. Each successful check caches the manifest in the worktree's git dir; offline, the cached manifest decides, so paths known to be owned stay protected (fail closed for those). With no cache at all there is nothing to check against: the commit is allowed, a warning is printed, and the gap is appended to `ferry-lanes-warnings.jsonl` in the worktree's git dir. The one way around it is explicit and logged: `FERRY_LANE_OVERRIDE="why" git commit ...` appends a signed line to `lanes/overrides/.jsonl` in the channel. Landing checks the same rule again over the whole lane diff, so `--no-verify` does not get around it; `ferry lanes land --allow-foreign why` is the logged escape there. ## Shared files `DECISIONS.md`, `docs/DECISIONS.md`, `README.md`, `CHANGELOG.md` (plus anything passed with `--shared`) belong to no lane. The rule: 1. **append only** to a log-like file (decisions, changelog); do not reflow or renumber; 2. edit it in your lane; it reaches `main` only through a landing, which rebases it; 3. git's `merge=union` driver (both sides' added lines kept) is used **only for files the lane declared append-only** (`--append-only DECISIONS.md`), recorded in the signed lane manifest and applied through a per-worktree attributes file and to the landing rebase. Nothing is written to the shared `info/attributes`. Union is wrong for a file edited in place (it would keep both versions of a changed line), so: `CHANGELOG.md` and `DECISIONS.md` are logs, declare them; `README.md` and `docs/*.md` are prose, do **not**: two lanes editing the same paragraph conflict, and the second rebases and resolves. Numbers come from `ferry counter next decision`, so entries do not clash (D30/D31 twice cannot happen); 4. a real conflict (both rewrote the same line) is a landing conflict: nothing changes, the second lane rebases and resolves. ## Landing `ferry lanes land `, run by the agent or by a person: 1. the lane must be committed (its working tree clean); 2. take the **landing lock**: a machine-local OS lock in the repo's git dir, plus a signed claim at `lanes/landing/..json` in the channel (15 minute expiry, earliest claim wins; the same agent on two machines has two claims, and a loser deletes only its own). A Syncthing conflict copy is never read as a claim; Only one landing runs at a time; others wait (`--wait`); 3. optionally fetch and fast-forward the base from the remote; 4. check ownership over the whole lane diff (`git diff -z --no-renames --name-only`, as the hook does, so non-ASCII names and a rename out of another lane's folder count); 5. **rebase in a scratch worktree** onto the base, so the lane's own files never pass through a half-rebased state; 6. conflict: `git rebase --abort`, remove the scratch worktree, list the files, post a signed notice to the lane's owner, exit non-zero. **The lane and the base are untouched**; 7. success: **re-check the lane under the lock** (same tip, no uncommitted change, no untracked file where a landed file arrives); if anything changed, nothing is touched and the landing is refused. Then keep the old tip at `refs/ferry/landed//