Skip to content

Give each worktree its own dev-server port #120

Closed

Description

@omartelo

Two worktrees of the same project both run pnpm dev, and both want 5173. The
second one loses. The setup script that just installed the dependencies has no
way to know it should have picked another number, because nothing tells it which
worktree it is running in.

Emdash treats the port as part of the worktree lifecycle: .emdash.json declares
setup/run/teardown and the runner injects $EMDASH_PORT, a per-task port, so a
script reads PORT=$EMDASH_PORT pnpm run dev. It is the one thing in the
category that handles the shared-port problem cleanly.

lich already has both halves of the seam:

  • a per-project worktree setup script (worktree.setup,
    internal/store/settings.go:130), run when the worktree is created;
  • per-session environment injection (Service.sessionEnv,
    internal/terminal/terminal.go:213) — where LICH_PORT, LICH_TOKEN and
    LICH_SESSION_ID already come from.

Shape

  • Derive a stable port per worktree — hashed from the checkout path into a
    range — so the number survives a restart and the same worktree always gets the
    same one.
  • Export it to the setup script and to every session spawned in that worktree.
  • Not LICH_PORT: that name is the hook listener (docs/hooks/README.md).
    LICH_WORKTREE_PORT.

Open

  • Range, and what happens on collision: probe for a free port when assigning, or
    trust the hash and let the script fail loudly?
  • Does the main checkout get one too, or only worktrees?
  • Range configurable per project, or fixed?
  • Windows: the setup script already skips its sh -c wrap there, but the
    variable can still be injected.

Related

Prerequisite for a preview surface in the dock — without a known port there is
nothing to point it at.

Activity

  1. added
    enhancementNew capability or improvement
    needs-shapingOpen questions to settle before implementing
    on Jul 28, 2026
  2. omartelo commented on Jul 29, 2026

    @omartelo
    CollaboratorAuthor

    Shipped in #126 (0192085). Recording the answers to the open questions here, since the issue was needs-shaping and the decisions otherwise only exist in the commit.

    Range, and what happens on collision — pure hash, no probing. FNV-1a of the cleaned checkout path folded into a fixed 1000-port window (internal/terminal/worktreeport.go). Nothing probes and nothing binds, so two checkouts can land on the same number and only the dev server started second finds out. Probing was the alternative and it costs exactly the stability that is the point: the previous server still holding the port would move the number on every restart. Recorded as a Known Ceiling.

    Does the main checkout get one too — yes. It is half of the original collision, and deriving from the session's start directory treats it like any worktree with no extra code.

    Range configurable per project — fixed, not configurable. 24000–24999: below the ephemeral range Linux allocates outbound connections from (32768), clear of the ports frameworks default to (3000, 5173, 8080), and clear of lich's own pinned listener (47821).

    Windows — the variable is injected normally; only the setup script's sh -c wrap is still skipped there.

    One thing worth knowing that the issue did not ask: the port derives from the session's start directory, not the repository root. In practice they are the same — sessions start at the worktree root — but a session started in a subdirectory would get a different number.

    The dock preview surface this unblocks is not covered here and has no issue yet.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew capability or improvementneeds-shapingOpen questions to settle before implementing

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions