agentlaneagentlane

3.2FeaturesMarkdown

Ports and dev servers

Services are the long-running processes an app needs, such as Metro, an API or a web dev server. Each gets a port from its type's block, passed as AGENTLANE_<NAME>_PORT, and up waits for its ready check before it moves on. doctor checks that the process on each port runs this worktree's code, not a server another checkout left behind.

Declaring a service

Services live under services in agentlane.json, by name. These are the example app's three:

agentlane.json

{
  "services": {
    "metro": {
      "type": "metro",
      "platforms": ["ios", "android"],
      "start": "EXPO_NO_TELEMETRY=1 exec node_modules/.bin/expo start --dev-client --port \"$AGENTLANE_METRO_PORT\" </dev/null",
      "ready": { "kind": "http", "path": "/status", "body": "packager-status:running" },
      "timeoutSeconds": 120
    },
    "api": {
      "type": "api",
      "start": "PORT=\"$AGENTLANE_API_PORT\" exec node api/server.mjs",
      "ready": { "kind": "http", "path": "/health" },
      "dial": { "argument": "-apiBaseUrl" }
    },
    "web": {
      "type": "web",
      "start": "API_URL=\"http://127.0.0.1:$AGENTLANE_API_PORT\" exec node_modules/.bin/vite web --port \"$AGENTLANE_WEB_PORT\" --strictPort --host 127.0.0.1 </dev/null",
      "ready": { "kind": "http", "path": "/" }
    }
  }
}

start runs the server in the foreground on the port in AGENTLANE_<NAME>_PORT: the service's name upper-cased, with dashes as underscores. up starts it in its own process group, from the folder that holds agentlane.json unless cwd says otherwise, and logs it to a file in the lane's folder.

A server has to take its port from that variable. One that hard-codes its port collides with the next lane's, and making it read the variable is the one change setup makes to such a server.

Port blocks and types

type names the block the port comes from: metro, api and web are built in, at 41100, 41200 and 41300. A type of your own declares its block with ports:

agentlane.json

{
  "services": {
    "worker": {
      "type": "worker",
      "ports": [41500, 41519],
      "start": "exec node worker.mjs --port \"$AGENTLANE_WORKER_PORT\"",
      "ready": { "kind": "tcp" }
    }
  }
}

The machine's own blocks win where they overlap, and no block may overlap the Android emulator's ports, 5554–5585. When a lane has exactly one service of a built-in type, it also gets that type's variable, such as AGENTLANE_API_PORT, whatever the service is called.

Ready checks

up moves on only when the service answers its ready check, and waits up to timeoutSeconds (60 by default).

CheckPasses when
HTTPGET path on the service's port returns status (200 by default), and the body contains body when you set it
TCPSomething accepts a connection on the port

Both connect to 127.0.0.1 unless host says otherwise. A service that exits or never answers fails the step with the last lines of its log.

Platforms

platforms limits a service to the apps that need it. In the example above, Metro serves only the mobile app, so up --platform web doesn't start it, and doctor and restart for the web leave it out. Leave platforms out to run a service for every platform.

Dialing a service from the app

A mobile app needs this lane's API address, not one baked into the build. dial passes it as a launch argument:

"dial": { "argument": "-apiBaseUrl", "value": "http://localhost:{port}" }

up launches the app with -apiBaseUrl http://localhost:41201, with {port} replaced by the lane's port. value defaults to http://localhost:{port}. A debug build reads the argument at launch. doctor's dials line checks that the running app got it.

This worktree's code

A port can answer with the wrong code: a dev server another checkout left running, or one started from the wrong folder. For each service, doctor checks the process listening on the port as well as the response. It fails when the listener isn't one up started for this lane, or when it runs in a folder outside this worktree, and names the pid and the folder:

FAIL  service web: pid 4021 is running; listening on 41300; ... the listener pid 4188 runs in ~/code/shop, outside this worktree ~/code/shop/.claude/worktrees/checkout-fix, so 41300 serves that folder's code

A listener started through a package manager, such as pnpm exec next dev, counts as the service's own, because agentlane looks at every process below the one it started.

Restarting with a new environment

FEATURE_CHECKOUT_V2=1 npx agentlane restart web

restart stops one service and starts it on the same port with the lane's variables plus that call's environment. It returns when the ready check passes, and leaves the device, the app and the other services as they are. Bring a worktree up has more.