← К публикациям

Как запускать параллельных coding-агентов и не ловить EADDRINUSE

Git worktree разделяет файлы, но не сетевые ресурсы. Когда агенты поднимают разрабатываемое приложение для локальной проверки и e2e-тестов, его серверы могут попытаться слушать один и тот же порт.

Слева несколько git worktree сталкиваются у одного localhost-порта, справа каждый worktree получает собственный стабильный порт

Зачем агентам отдельные worktree

git worktree создаёт дополнительную рабочую директорию того же репозитория, в которой можно checkout-ить другую ветку. Git-объекты и история остаются общими, но у каждого worktree свои файлы и индекс Git.

Это нужно, когда несколько агентов или разработчиков одновременно выполняют независимые задачи. В одной рабочей директории их незакоммиченные изменения, область подготовки к коммиту и запуск тестов смешиваются: один агент может проверить полуготовый код другого или закоммитить чужие файлы. Отдельный worktree даёт каждой задаче и ветке собственную директорию; конфликты всё равно возможны, но проявляются при осознанном merge, а не во время работы.

Worktree не заменяет декомпозицию: если агенты меняют одну часть системы, смысловой конфликт останется. Но он позволяет вести независимые потоки без постоянного stash и checkout. Практический пример параллельных агентных сессий — у incident.io.

Когда у меня одновременно работают несколько coding-агентов, каждый получает свой git worktree. Один меняет авторизацию, второй правит интерфейс, третий гоняет end-to-end тесты. На уровне Git они изолированы. На уровне операционной системы все они по-прежнему делят один localhost.

Из-за этого агент, который запускает очередной Vite, Rails, Next.js или Playwright web server, может получить:

Error: listen EADDRINUSE: address already in use :::3000

Сама ошибка несложная. Проблема в её операционной цене: агент отвлекается от задачи, ищет процесс, решает, можно ли его убить, меняет порт в одной команде и забывает передать его тестам. В худшем случае он останавливает чужой сервис.

Почему случайный порт не решает всю задачу

Первая идея — попросить операционную систему выдать любой свободный порт. Для одного процесса этого часто достаточно. Но в agentic workflow порт нужен не одной строке.

Его должны знать:

  • команда запуска dev server;
  • browser-тесты и их BASE_URL;
  • человек, открывающий preview;
  • следующая сессия агента;
  • иногда другие сервисы того же worktree.

Анонимный порт хорош внутри одного процесса. Для координации нужен адрес, который можно повторно получить по имени проекта.

Какой контракт нужен

Я свёл задачу к четырём свойствам.

1. Порт свободен в момент выдачи

Аллокатор не просто инкрементирует число. Он проверяет, что порт не занят процессом. Это не устраняет все гонки: между выбором порта и bind (моментом, когда сервер его занимает) всё ещё есть короткое окно. Поэтому команду запуска лучше строить как одну операцию.

2. Порт стабилен для директории

Один и тот же worktree должен получать тот же порт. Тогда preview URL не меняется после каждого перезапуска, а тесты и агенты могут повторно найти его без копирования числа в промпт.

3. Одна директория может иметь несколько адресов

Монорепозиторий может поднимать frontend, API и локальную базу. Поэтому ключ аллокации — не только директория, но пара (directory, name).

4. Параллельные вызовы не портят реестр

Два агента могут запросить порт одновременно. Запись о выделении должна быть сериализована. В port-selector для этого используется file locking на Unix. На Windows сборка есть, но этой гарантии пока нет — это важное ограничение, а не мелкая сноска.

Установка

На macOS проще всего установить утилиту через Homebrew:

brew tap dapi/tapbrew install port-selector

Проверка:

port-selector3000

Повторный вызов из той же директории вернёт тот же порт. Из другого worktree — другой.

Dev server

PORT="$(port-selector)"npm run dev -- --port "$PORT"

Получение порта и запуск стоят рядом. Это сужает окно между проверкой и bind.

Playwright

PORT="$(port-selector --name e2e)"BASE_URL="http://127.0.0.1:$PORT" npx playwright test

Если тестовый сервер стартует в конфигурации Playwright, туда нужно передать тот же порт. Главный принцип: не выбирать его дважды.

direnv

Для worktree удобно выделять порт при входе в директорию:

.envrc
export PORT="$(port-selector --name web)"

Тогда обычная команда npm run dev может читать PORT из окружения, а агенту не нужно запоминать число.

Несколько сервисов

export WEB_PORT="$(port-selector --name web)"export API_PORT="$(port-selector --name api)"export DB_PORT="$(port-selector --name db)"

Имя — часть ключа. Повторный port-selector --name api из той же директории вернёт прежний API-порт, а не создаст новый.

Что записать в инструкции агента

Важно не просто установить CLI, а убрать из промпта выбор порта. В AGENTS.md или аналогичном файле достаточно короткого контракта:

AGENTS.md
## Local servers

Before starting any process that binds localhost, allocate a stable port
from the repository root:

PORT="$(port-selector --name web)"
npm run dev -- --port "$PORT"

Выделение происходит из корня конкретной директории worktree: путь — часть идентичности.

Как подключить skill агенту

В репозитории port-selector есть skill с тем же контрактом. Он применяется, когда агент запускает локальный сервер, preview, тестовый runner или сталкивается с занятым портом.

Сначала установите CLI, затем из корня проекта подключите skill:

npx skills add dapi/port-selector --skill port-selector

Если skill нужен во всех проектах, добавьте --global. После установки начните новую сессию агента, чтобы он прочитал SKILL.md.

Что не надо делать

Не зашивать порт в каждую команду

Значение 3000 быстро размножается по package.json, Playwright config, README и промптам. После первой коллизии приходится искать все копии.

Не запрашивать отдельный порт для теста

У приложения и теста должен быть один адрес. Ошибка возникает, когда сервер получает порт с именем web, а тест отдельно запрашивает порт с именем e2e:

# Сервер слушает, например, 3001.
PORT="$(port-selector --name web)"
npm run dev -- --port "$PORT"

# Тест получает другой порт, например, 3002, и не находит приложение.
BASE_URL="http://127.0.0.1:$(port-selector --name e2e)" npx playwright test

Имя — часть идентичности аллокации, поэтому web и e2e намеренно возвращают разные порты. Выберите имя один раз и передайте уже полученный PORT всем потребителям:

PORT="$(port-selector --name web)"npm run dev -- --port "$PORT" &BASE_URL="http://127.0.0.1:$PORT" npx playwright test

Если Playwright сам запускает сервер через webServer, передайте тот же PORT в его конфигурацию, а BASE_URL соберите из этого же значения.

Границы решения

port-selector не контейнер и не прокси. Он не разделяет сети и не держит socket от имени сервиса. Он решает более узкую задачу: делает выбор локального порта повторяемым и управляемым.

На macOS и Linux этого достаточно для большинства локальных dev servers. На Windows нет file-locking гарантии. На macOS поиск владельца процесса ограничен по сравнению с Linux. Если нужна жёсткая network isolation или атомарная передача socket, нужен другой уровень изоляции.

Итог

Проблема не в числе 3000. Она в ложном ощущении полной изоляции: отдельный worktree даёт задаче собственные файлы, но локальный сервер, тесты и preview по-прежнему работают в общей сети localhost.

Без явного контракта каждый запуск заново решает, какой порт использовать. Один агент занимает 3000, другой получает EADDRINUSE, а после случайной замены порта browser-тесты и preview могут продолжить ходить по старому адресу.

Слева сервер и тест отдельно запрашивают порты с именами web и e2e и получают разные адреса. Справа один порт с именем web передаётся серверу, тесту и preview.

port-selector не изолирует сеть и не устраняет гонку между выбором порта и bind. Он решает более узкую, но повторяющуюся проблему: один раз связывает директорию и имя сервиса со свободным портом, а сервер, тест и preview используют уже полученное значение. Поэтому агенту не нужно «разруливать порт» при каждом запуске, а несколько worktree могут параллельно проверять свои изменения на одной машине.

Ссылки

Хотите внедрить это у себя?

Помогаю командам перейти на агентную разработку: как советник, через обучение команды или внедрение изменений с проверкой эффекта по данным. Короткие заметки между статьями выходят в Telegram-канале.

Работать со мной или канал «Жизнь стартапа в стране ИИ»