Skip to content

feat(dev): one-command worktree adoption with per-worktree ports - #162

Merged
jonocodes merged 3 commits into
mainfrom
setup-worktree-support
Sep 23, 2026
Merged

jonocodes merged 3 commits into
mainfrom
setup-worktree-support

Conversation

@jonocodes

Copy link
Copy Markdown
Owner

Summary

Add automated setup and port management for git worktree checkouts. 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

    • Assigns the lowest free offset (0 for primary, 1+ for siblings) applied to all three port bases at once, so a worktree's ports stay mentally grouped (8766/5174/8976)
    • Wires .envrc to the primary checkout's flox environment for toolchain sharing, while keeping each worktree's own .venv (isolated, editable installs)
    • Copies gitignored-but-host-wide files (TLS certs) from primary to avoid sudo re-provisioning per worktree
    • Idempotent: safe to re-run, used as both adopt and repair
  • Justfile: Integration and new recipes

    • set dotenv-load := true loads per-worktree ./.env automatically
    • just worktree-adopt [--no-install] [--force] — make this worktree dev-ready
    • just worktree-doctor — diagnose why a worktree isn't working; every failure prints its fix
    • just worktree-list — show all worktrees, their ports, and readiness
    • just worktree-create BRANCH — git worktree add then adopt in one go
    • Added DECKD_E2E_PORT (default 8975) for Playwright fixture daemon, so two worktrees can run just test-all simultaneously
  • client/playwright.config.ts: Use DECKD_E2E_PORT for fixture daemon and its tmpdir, avoiding collisions between concurrent e2e runs

  • Documentation: Comprehensive guides

    • ONBOARDING.md: Worktree section rewritten with port tables, toolchain/venv isolation model, and all caveats
    • GUIDE.md: Updated to reference just worktree-adopt instead of manual env vars
    • REFERENCE.md: Added development ports and Justfile recipes tables
  • tests/test_worktree_setup.py: Port allocation and diagnosis over real git worktrees

    • Tests idempotency, sibling collision avoidance, --force reassignment, .env copying, and error detection
  • .gitignore: Ignore .env (per-worktree, written by just worktree-adopt)

Why

Worktrees share the host's port space; just dev in a second checkout collides with the first and fails. Previously, docs suggested DECKD_PORT=8766 VITE_PORT=5174 by 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 --force repairs hand-edited .env files.

Testing

All port allocation and collision cases covered by integration tests over real git worktree checkouts.

jonocodes and others added 3 commits September 22, 2026 21:36
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.
@jonocodes
jonocodes merged commit 5ad86a2 into main Sep 23, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant