feat(dev): one-command worktree adoption with per-worktree ports - #162
Merged
Merged
Conversation
Code already needed nothing for `git worktree` — every path resolves from `Path(__file__)`. What a fresh checkout lacked was the gitignored scaffolding (.envrc, .flox/, .venv/, client/node_modules/, client/.tls/) and a port assignment that doesn't collide with its siblings, so a new worktree couldn't even run `just setup` without a toolchain. Add scripts/worktree.sh plus four recipes, using "adopt" rather than "create" because the checkout usually already exists — an agent harness (Paseo, Cursor) or a plain `git worktree add` made it: just worktree-adopt # ports, .envrc, shared files, `just setup` just worktree-doctor # why isn't this worktree working? just worktree-list # every worktree, its ports, ready or not just worktree-create # the from-scratch case Adopt is idempotent, so it doubles as the repair command; it must never renumber a worktree out from under a running daemon. Ports move as a group by offset. The primary checkout is always offset 0, so :8765/:5173/:8975 and the main checkout is unchanged. `set dotenv-load` means every recipe picks up the per-worktree ./.env with no env-var juggling; an explicit `DECKD_PORT=9000 just dev` still wins. Also adds DECKD_E2E_PORT and suffixes Playwright's throwaway layouts dir with it. Previously the fixture daemon was pinned to :8975 and shared /tmp/deckd-e2e-layouts, so two worktrees could not run `just test-all` concurrently. Toolchain: the generated .envrc runs `flox activate -d <primary>`, since direnv's `use flox` requires a local .flox/ that a worktree never has. flox leaves $PWD alone, so the manifest's $PWD/.venv hook still finds this worktree's venv. That split is load-bearing — `uv pip install -e .` bakes in an absolute path, so a shared venv would silently point `deckd` at whichever worktree installed last. The .envrc is only written when the primary itself uses direnv/flox; the repo prescribes neither. tests/test_worktree_setup.py drives the script over real git worktrees in tmp: collision-freedom, idempotency, --force repair, duplicate-offset detection, and the .envrc/copy behaviours. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
test-py312 is a Python-only job — no setup-node, no `npm ci` — so the
bare `npx tsc` in this test silently downloaded whatever TypeScript npm
had published that day. The check was never hermetic, and 5.9 duly broke
it: TS5112 ("tsconfig.json is present but will not be loaded if files are
specified on commandline") fires because the run used client/ as its cwd.
Not one line of the emitter changed. main has been red on this since
2026-09-20.
Invoke client/node_modules/.bin/tsc directly so the emitter is checked
against the version the repo actually pins (^5.5.3), and skip when the
client toolchain isn't installed — the full `test` job always has it.
Run from the tempdir too, so a future TypeScript bump doesn't trip over a
neighbouring tsconfig.json again.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…he venv
Running dev alongside an installed deckd service (or several dev checkouts)
surfaced three collisions plus one broken assumption:
- The primary checkout was assumed to own offset 0. On a machine whose
installed service holds :8765, `just dev` there failed on address-in-use
forever. `worktree-adopt` now checks whether offset 0 is actually free and
moves the primary to the lowest free offset like any other checkout,
writing a `.env`. `offset_of` still treats the primary's implicit 0 as
owned, so siblings keep routing around it; a new `env_offset` separates
"implicitly owns 0" from "has claimed an offset".
- `just status` / `diag` / `layouts` / `metrics` ran bare `deckctl`, whose
default :8765 silently answered from the installed daemon. They now pass
`--port {{DECKD_PORT}}`.
- `just smoke` hardcoded :18765, so two checkouts collided. New
`DECKD_SMOKE_PORT` (base 18765) flows `.env` -> Justfile -> smoke.py,
completing the per-offset port group and letting two checkouts run
`just test-all` concurrently.
- The generated `.envrc` relied on the flox manifest's `[profile]` hook to
put `$PWD/.venv/bin` on PATH, but flox sources `[profile]` only for an
interactive shell — direnv's non-interactive env dump never carried it.
In an activated worktree `deckd-dev` was therefore not found and
`just dev-daemon` failed. The `.envrc` now adds `.venv/bin` (and exports
VIRTUAL_ENV when the venv exists) itself.
Tests grow to 17 in tests/test_worktree_setup.py, with a
`DECKD_WORKTREE_BUSY_PORTS` seam so allocation is exercised without binding
real sockets or depending on the host's listeners. Docs updated: the port
table gains the smoke column, the primary-can-move rule, and a section on
running dev alongside an installed instance.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Add automated setup and port management for
git worktreecheckouts. A fresh worktree now requires just one command —just worktree-adopt— to become dev-ready, with no manual env-var juggling.What changed
scripts/worktree.sh: Port allocator and worktree lifecycle management.envrcto the primary checkout's flox environment for toolchain sharing, while keeping each worktree's own.venv(isolated, editable installs)Justfile: Integration and new recipes
set dotenv-load := trueloads per-worktree./.envautomaticallyjust worktree-adopt [--no-install] [--force]— make this worktree dev-readyjust worktree-doctor— diagnose why a worktree isn't working; every failure prints its fixjust worktree-list— show all worktrees, their ports, and readinessjust worktree-create BRANCH—git worktree addthen adopt in one goDECKD_E2E_PORT(default 8975) for Playwright fixture daemon, so two worktrees can runjust test-allsimultaneouslyclient/playwright.config.ts: UseDECKD_E2E_PORTfor fixture daemon and its tmpdir, avoiding collisions between concurrent e2e runsDocumentation: Comprehensive guides
just worktree-adoptinstead of manual env varstests/test_worktree_setup.py: Port allocation and diagnosis over real git worktrees--forcereassignment,.envcopying, and error detection.gitignore: Ignore
.env(per-worktree, written byjust worktree-adopt)Why
Worktrees share the host's port space;
just devin a second checkout collides with the first and fails. Previously, docs suggestedDECKD_PORT=8766 VITE_PORT=5174by hand — error-prone and inconvenient. Missing scaffolding (.venv,node_modules, TLS certs) meant every new worktree required setup overhead.This automates it. One command, idempotent, with built-in diagnosis (
just worktree-doctor). The allocator is deterministic — siblings never collide, the primary keeps its defaults, and--forcerepairs hand-edited.envfiles.Testing
All port allocation and collision cases covered by integration tests over real
git worktreecheckouts.