Первым выстрелил порт. Браузерные тесты поднимают дев-сервер и ходят в него, а playwright умеет переиспользовать уже запущенный сервер, если на нужном адресе кто-то отвечает. Само по себе это удобно и у меня включено до сих пор. Проблема была в адресе - сервер поднимался на обычном порту разработки, одном и том же для всех чекаутов. И прогон, запущенный из worktree, преспокойно тестировал приложение из главного чекаута, другой ветки. Он сообщил о дефекте, который в этой самой ветке был уже починен. Полчаса ушло на разбор несуществующего бага. Починка стала такой - каждому свой порт, а дев-серверу - флаг --strictPort (это флаг Vite, не Playwright), чтобы занятый порт означал ошибку, а не тихий переход на следующий свободный. Занятый порт должен кричать (сейчас на этом слове вдруг осознал, что понахватался терминологии от агентов, раньше я бы и представить не мог, что закрытый порт может "кричать" :)). Переиспользование при этом осталось, но теперь оно переиспользует только свой собственный сервер на своём собственном порту.
Иронично, что через три недели этот же флаг уронил чужой PR. CI у меня живёт на этой же машине, где работают агенты, на своих раннерах, и в какой-то момент чей-то прогон покраснел с сообщением "порт 5174 уже используется" - порт держал локальный прогон одной из сессий. К коду в том PR это не имело ни малейшего отношения. То есть сначала мы сделали порт строгим, а потом обнаружили, что строгий порт без изоляции сам становится источником конфликтов. Развели порты по ролям - для рабочей сессии один, для CI - другой, между двумя своими worktree - задавать явно.
Вторым выстрелил путь. Контрактные тесты ищут схемы в соседнем репозитории относительно рабочего каталога, а worktree лежит на уровень глубже обычного чекаута - и путь стал указывать в пустоту. Прогон давал 215 упавших тестов вместо 2191 прошедшего. Ошибка, при этом, сообщала и лечение, но две сотни красных строк повергли агента в шок и он начал преживать, что сломал вообще весь код. Починили тем, что поиск стал подниматься по дереву каталогов вверх, пока не найдёт корень нужного репозитория, а маркером сделали сам файл спецификации, а не имя папки. Иначе пустая папка с подходящим именем, оказавшись ближе, выигрывала бы просто по расстоянию.
Третьим выстрелило то, что каталог агента, в котором запускается фоновая команда, - не то состояние, на которое можно рассчитывать в следующей команде. У моего раннера после фоновой задачи каталог переднего плана возвращается туда, где был. Агент запускает длинный прогон в фоне, потом выполняет обычную команду - и правит файлы уже не в своём worktree, а в главном чекауте. Молча, без единой ошибки. Однажды за одну сессию такое случилось трижды. Механика та же, что и в первом инциденте, только виноват уже не сосед, а собственный дрейф. Лечится тем, что каждая команда начинается с перехода в свой worktree, а не только первая в цепочке. И, поскольку такое правило держится исключительно на внимательности, теперь стоит хук, который просто отказывает в записи в главный чекаут репозитория, если у того есть живое второе дерево. Чтение и уборку после мержа он пропускает, всё остальное - нет.
Последнее, о чём хочется рассказать про текущий флоу - то, как открываются пулл-реквесты. Агент у меня не открывает их сам, он готовит ветку, коммит и push, а дальше отдаёт мне ссылку на страницу сравнения веток с уже подставленными заголовком и описанием. Я открываю, при желании правлю прямо в форме и жму кнопку. Смысл в том, что создание PR - это точка, где я могу посмотреть на работу глазами перед тем, как она поедет в мастер. Честно признаюсь, что реально ревью я провожу всё реже и реже, больше полагаясь на выросшие вокруг процесса автоматические проверки. Плюс база всегда мастер - отдельное правило, появившееся после того, как GitHub однажды влил PR в другую feature-ветку, которая к тому моменту отстала, и работа просто не доехала.