feat(lanes): let claude and codex lanes coexist at the same cwd

Repo/Adopt mode (POST /api/lanes/ensure) treated lanes.cwd as globally
unique, so adopting a path already driven by one CLI silently ignored a
different tool choice. Uniqueness moves to the composite (cwd, tool):
one directory can now back a claude lane and a codex lane side by side.

- server/db.js: drop the column-level UNIQUE on cwd, migrate existing
  DBs via table rebuild, add idx_lanes_cwd_tool.
- resolveLaneByCwd gains an optional tool filter; hook ingestion always
  pins to "claude" since Codex has no hook mechanism here, so a hook
  event never touches a codex lane sharing the path.
- POST /ensure resolves the existing owner by (cwd, tool) instead of
  cwd alone, so a different tool creates a sibling lane.
- LaneConsolePane's cwd-only lane lookups now also match tool, so a
  shared cwd doesn't attach a run to the wrong sibling lane.

Worktree mode is unaffected (already allocates a unique directory per
lane). docs/LANES.md and CLAUDE.md updated to describe the new
composite uniqueness.
This commit is contained in:
2026-08-27 13:06:08 +07:00
parent f8fba0a51d
commit ccf2fdb8eb
9 changed files with 530 additions and 17 deletions
+1
View File
@@ -37,6 +37,7 @@ A **lane** is a durable unit of parallel agent work — one working directory, m
- **Never build a git command as a shell string.** Lane git operations use `execFile("git", [...args])` with an explicit argument array (see `server/lib/worktree.js`'s `git()` helper), never a concatenated/interpolated string passed to a shell.
- **A lane's `tool` is a lane fact, immutable, and never read from a request.** `lanes.tool` (`claude` | `codex`, default `claude`) is set by `createLane` only — it is deliberately absent from `PATCHABLE`, so no PATCH can flip the CLI under a live pane. Every spawn path (`POST /api/lanes/:id/start`, `POST /api/run`) takes the tool from the lane row and ignores whatever the body says; the run panel's Codex-only fields (`sandbox`, `approval`, `resumeLast`) are validated against the fixed allowlists in `server/lib/pty-run.js` before they reach an argv, and the bypass flags (`--dangerously-bypass-approvals-and-sandbox`, `--dangerously-bypass-hook-trust`) are never emitted. `GET /api/run/binary?tool=` resolves the probed binary name from that same allowlist — the query string reaches `which`.
- **Adopted lanes are not destroyable.** A lane with `kind === "adopted"` may never have its worktree reset or removed; "removing" one only drops the dashboard's own record of it. This is enforced in code (`assertDestroyable`'s first check) — do not add a path that bypasses it.
- **A `cwd` can back at most one lane per tool, not one lane overall.** Uniqueness is the composite `idx_lanes_cwd_tool` (`cwd`, `tool`), not `cwd` alone — Repo/Adopt mode (`POST /api/lanes/ensure`) lets the same directory carry a `claude` lane and a `codex` lane side by side, shown as two separate cards. `resolveLaneByCwd(cwd, tool)` takes an optional `tool` filter for exactly this; hook ingestion (`touchLaneFromHook`, `resolveLaneForWork`) always pins it to `"claude"` because Codex has no hook mechanism in this codebase, so a hook event must never touch a `codex` lane sharing the path. Worktree mode is unaffected — it already allocates a unique directory per lane.
See `docs/LANES.md` for full guide: stage reporting, custom templates, lane actions, and the five node states.