Skip to content

Dev Serve ​

Start a dev server on a safe port. One command, fully automated — handles port allocation, worktree ownership, and race conditions.

Replaces the old ensure-dev-server script. Use dev-serve whenever starting a dev server, especially in multi-worktree or multi-agent environments.

Usage ​

bash
dev-serve <port> <command>

The allocated port is exported as $PORT. Most frameworks read it automatically. If yours needs an explicit flag, reference $PORT:

bash
dev-serve 3000 "npm run dev"
dev-serve 3000 "vite --port $PORT"
dev-serve 8000 "python -m http.server $PORT"

On success the script prints:

ACQUIRED <port> <pid>
http://localhost:<port>

…and opens the URL in the default browser (suppress with SERVE_OPEN=0).

The script lives alongside this file as an executable: skills/dev-tasks/dev-serve

How it works ​

From the preferred port, the script walks upward:

  1. Port owned by this worktree → reuse it as-is. Print ACQUIRED with the existing PID. No new process is started.
  2. Port owned by another worktree → skip, increment.
  3. Port is free → attempt to start the server.
  4. Server binds successfully → print ACQUIRED.
  5. Server dies on startup (race lost) → increment, retry.

A free port is advisory. The start attempt is the real lock. If another process grabs the port between the check and the bind, the script detects the failure and moves on.

dev-serve never kills automatically — not even a server it owns. If your own server is genuinely broken (hung, crashed, stuck compiling), kill the PID yourself and run dev-serve again.

Port ownership ​

Ownership is determined by comparing the listener process's working directory against the current worktree root (from git rev-parse --show-toplevel). If the process's cwd is inside the current worktree, it's MINE. Otherwise it's OTHER.

dev-serve reuses MINE ports and skips OTHER ports. It never kills either.

This is unchanged by the container layout: each worktree is its own toplevel, so <repo>/main and <repo>/feature/auth own their ports separately, exactly as nested worktrees did. Run it inside a worktree though. The container directory is not a work tree, so git rev-parse --show-toplevel there fails with fatal: this operation must be run in a work tree and ownership cannot be determined at all.

Diagnostic ​

Inspect a port without starting anything:

bash
dev-serve check <port>

Prints one of:

OutputMeaning
FREENo listener on this port
MINE <pid>This worktree's server — reused on start
OTHER <pid> <cwd>Another worktree's server — never touched

Environment variables ​

VariableDefaultPurpose
SERVE_MAX_ATTEMPTS50Max ports to try
SERVE_SETTLE_MS3000Wait time for bind
SERVE_WORKTREEautoOverride worktree root
SERVE_OPEN1Open the URL in a browser on success (0 to suppress)

Rules ​

The core principle: dev-serve never kills. A listening server is assumed to be doing its job until the human decides otherwise. Running dev-serve against an existing MINE port is a no-op that returns the URL.

Never blind-kill ​

Never run kill $(lsof -ti tcp:3000). Never run killall node or pkill -f vite. If a server is genuinely broken and you need it gone, target its specific PID by hand, then run dev-serve again.

Never assume "port in use" means your server ​

Another worktree, another agent, or a completely unrelated process may be listening. dev-serve classifies every occupied port before acting.

Never check-then-start as two steps ​

A port that's free when you check may be taken when you bind. dev-serve treats the start attempt as the real lock and retries on failure. Don't write your own two-step check-then-start logic.