Одобрение — это и есть «поехали»
В июле я писал, что параллельные агентные сессии дали меньше, чем я рассчитывал, и что дело не в агентах: «ограничением была не пропускная способность агентов, а моя». Чаще всего я вёл одну хорошо налаженную петлю, а не несколько сразу, и параллельный режим включал при необходимости, а не по умолчанию.
Две недели назад я закончил пост об обвязке тем, куда вели все изменения: убрать человека из петли везде, где он нужен был только потому, что что-то не записали или не превратили в исполняемый шаг.
Этот пост — о самом большом шаге в эту сторону. У Ritmolux теперь есть дирижёр (conductor): программа на Node, которая берёт одобренные планы из очереди и доводит каждый до слияния в main, не требуя от меня ни одного действия. Он открывает дорожку в отдельном git worktree, запускает свежие headless-сессии claude -p для реализации фаз плана, сверяет с git всё, что они о себе заявляют, прогоняет собственную проверку, запускает отдельного рецензента и отдельную сессию закрытия, перематывает main вперёд и удаляет дорожку. Всё, что он не может решить сам, он паркует, а дорожка берётся за следующий план.
Он никогда не пушит. Всё, что он делает, остаётся на локальной машине, пока я не прочитаю, что произошло, и не запушу сам.
Ему двенадцать дней. За это время в него вошло 130 коммитов, около 5 700 строк на Node и в шаблонах промптов и порядка двадцати ADR, большинство из которых вносят поправки в первый. Это соотношение и есть вся история: исходный замысел был в целом верным, а почти каждая поправка появилась после прогона, который показал, где он ошибался.
Четыре действия, в которых нечего было решать
ADR, с которого всё началось, ADR-0205, открывается инвентаризацией. Каждый план проекта стоил мне четырёх ручных вмешательств: открыть дорожку в worktree; написать «поехали» сессии dev, план которой я уже одобрил; когда dev выведет указание на закрытие, запустить свежую сессию /architect; после закрытия перемотать main и удалить дорожку.
Ни в одном из четырёх не было решения. 14 сентября я за один присест одобрил одиннадцать планов, и при таком темпе скорость задавала уже не работа, а эти вмешательства. Одиннадцать планов по четыре шва — сорок четыре раза, когда мне нужно было оказаться за клавиатурой в нужный момент ради действий, суть которых была решена заранее.
Но у каждого шва было письменное обоснование в одном из прежних решений. Поэтому начинать надо было не со строительства. Сначала нужно было отделить каждый довод от механизма, который его по случайности воплощал, и посмотреть, устоит ли довод, если механизм убрать.
«“Поехали” — это одобрение». Верно, но одобрение, которое фиксирует «поехали», к тому моменту уже дано: план запускается, только если в его файле стоит Status: approved. Второе подтверждение защищало от другого риска — что сессия выйдет за рамки своей задачи. От этого риска так же хорошо защищает сессия, которой дали точный диапазон фаз и велели на нём остановиться. Поэтому для плана, который ведёт дирижёр, одобрение и есть «поехали». Вне дирижёра сессия dev, запущенная вручную, по-прежнему пересказывает план и ждёт команды.
«Рецензия при закрытии бесполезна, если её делает та же сессия, что писала код». Тоже верно, и ключевое слово здесь — та же. Свежесть — свойство контекста рецензента, а не того, кто открыл сессию. Рецензент, запущенный отдельным процессом, которому передали только путь к плану и дорожку, ничего не знает о ходе реализации — ровно как свежая ручная сессия. И получает то, чего у ручного шва не было никогда. Когда рецензента запускал я, он оставался свежим ровно до тех пор, пока я удерживался от того, чтобы вставить ему краткое содержание. Дирижёр не может передать рецензенту краткое содержание: у него его просто нет.
«human-фазу некому передать». Верно, и тут ничего не изменилось. Фаза, где я нужен у эталонной машины или у монитора, чтобы оценить картинку, по-прежнему требует меня.
Два довода из трёх устояли без своего механизма. В этом и состоит всё обоснование дирижёра, и записать его стоило до первой строки кода, потому что это заодно и список того, что дирижёр не должен незаметно размыть.
Альтернативы: каждая переносила вмешательство, а не убирала его
В ADR семь альтернатив. Три стоит пересказать: каждая выглядит очевидным решением, и каждая отпадает из-за факта, а не из-за вкуса.
Workflow-скрипт внутри сессии — родной механизм инструмента, и написать его быстрее всего. Не подходит он потому, что workflow завершается вместе с запустившей его сессией Claude Code и возобновляется только в ней же. Очередь, которая живёт днями и паркуется в ожидании человека, требовала бы от меня держать сессию живой. Вмешательство не исчезает, а переезжает.
Субагенты с изоляцией в worktree не подходят по двум причинам. Агенту в изолированном worktree запрещены git-операции над основным чекаутом, так что слить ветку он не может. А субагент живёт внутри породившей его сессии — та же проблема, что с workflow.
Agent SDK в качестве движка даёт типизированный API сессий и колбэк разрешений на каждый вызов. От него пока отказались: он добавляет пакет в репозиторий, где на счету каждая зависимость, а того же контроля можно добиться через claude -p, файл настроек и хуки, которые в проекте уже есть. В ADR записано, когда вернуться к вопросу: если разбор потока событий CLI станет главной статьёй затрат на сопровождение дирижёра. Условие достаточно конкретное, чтобы я заметил, когда оно наступит.
Остаётся обычная программа, которая запускает другие программы. Вариант наименее изобретательный — и единственный, чей жизненный цикл не привязан к сессии, которую мне приходится держать открытой.
Конвейер
flowchart TB q["queue.json<br/>одобренные планы по дорожкам"] lane["открыть дорожку в worktree"] ready["проверка готовности<br/>architect, только чтение"] impl["реализация<br/>одна сессия dev на серию фаз одного владельца"] verify["сверить заявление с git"] gate["влить main, прогнать проверку"] review["рецензия<br/>architect, свежий процесс"] fix["раунд исправлений<br/>не больше двух"] close["закрытие<br/>architect, под блокировкой закрытия"] ff["перемотать main, удалить дорожку"] park["парковка<br/>запись во входящие, дорожка идёт дальше"] q --> lane --> ready --> impl --> verify --> gate --> review review -- "блокеры или мажоры" --> fix --> review review -- "чисто" --> close --> ff ready -. "plan_wrong" .-> park verify -. "disagreement" .-> park gate -. "красная после одной починки" .-> park review -. "всё ещё не проходит" .-> park
Каждый блок, где работает модель, — отдельный процесс claude -p с системным промптом, дополненным из шаблона, списком разрешённых инструментов, --permission-mode dontAsk и лимитом трат. В конце каждый выводит ровно один блок кода с меткой rlx-outcome и одним JSON-объектом внутри: какие фазы завершены и какие коммиты сделаны — либо на какой фазе сессия запарковалась и почему. Этот блок — единственное, что дирижёр читает из ответа модели. Всё остальное он берёт из репозитория.
Две блокировки упорядочивают то, что двум дорожкам нельзя делать одновременно. Блокировка закрытия удерживается от начала закрытия до перемотки main, чтобы повышение версии и тег всегда попадали на тот main, относительно которого были вычислены. Блокировка тестов оборачивает каждый полный прогон тестов: две дорожки, одновременно гоняющие GPU-тесты, — ровно та нагрузка, при которой некоторые тесты, завязанные на часы, уже падали, хотя по отдельности проходили. Хук, активный только в сессиях дирижёра, отклоняет вызов cargo nextest в обход блокировки.
И ещё один хук действует на все сессии в репозитории, с дирижёром или без: он запрещает git push, reset --hard, rebase и commit --amend. «Никогда не пушить» раньше было просто фразой в CLAUDE.md. Дирижёр превратил её в механизм, потому что нельзя рассчитывать, что headless-сессия эту фразу прочла.
Headless-режим: проверено опытом, а не документацией
Автомата состояний у дирижёра ещё не было, а зонд уже был. spike/probe.mjs запускает несколько коротких сессий claude -p в одноразовом worktree и записывает, что CLI сделал на самом деле. Загружается ли навык проекта в headless-сессии? Приходит ли запрет от хука в сессию понятной ошибкой инструмента, или всё зависает? Выполняется ли разрешённая команда оболочки и отклоняется ли запрещённая без зависания? Что содержит финальное событие result? Что происходит, когда сессия упирается в лимит трат? Не оставляет ли git worktree remove открытый дескриптор?
Каждый ответ — строка в spike/README.md с датой и версией CLI, на которой он получен. На версии CLI, для которой таблицу не составляли, дирижёр работать отказывается. Тогда это казалось перестраховкой. Пригодилось на второй же день.
CLI обновился сам, за ночь
Утром в день первого настоящего пилота CLI сам обновился с 2.1.270 до 2.1.272, и предварительная проверка отказалась открывать дорожку. Чтобы снять запрет, пришлось прогнать зонд (пятнадцать центов, две сессии на маленькой модели), глазами сверить его JSON с таблицей, дописать версию в список проверенных и закоммитить — ничего из этого дорожка сама сделать не может.
Защита сработала правильно, и было ясно, что это будет повторяться, причём когда угодно. ADR-0208 её ослабил: версия с теми же major и minor, что у проверенной, но с бо́льшим patch, запускается с предупреждением, которое висит на странице статуса, пока её кто-нибудь не проверит. Цену ADR называет честно, и этому я бы подражал: этот CLI почти каждый релиз помечает как patch, так что правило почти при каждом обновлении превращает защиту в предупреждение. Причём риск именно здесь уже был зафиксирован: между этими двумя патчами показатель использования лимитов переехал в другое поле.
Поэтому полезный вопрос звучал не «проверена ли версия», а какое тихое изменение прошло бы незамеченным. Большинство — не прошло бы. Нет события результата — план паркуется; нет блока итога — паркуется; блок кривой — тоже: читатель потока и так при любом сомнении останавливается. Опасны изменения, при которых сбой, наоборот, всё пропускает, и такое изменение одно: хуки проекта не запустились. А хуки — это блокировка тестов, запрет пуша, запрет массового git add и запрет строк об авторстве. Сессия без них всё равно завершается корректным блоком итога, и ничто из того, что читает дирижёр, этого не покажет.
Поэтому теперь каждая сессия дирижёра обязана доказать, что её хуки сработали. Хук блокировки тестов дописывает строку в журнал шага, путь к которому дирижёр передаёт через переменную окружения. По завершении сессии рядом с транскриптом, где есть хоть один вызов оболочки, обязан лежать журнал хука, а событие init в потоке обязано перечислять навык, который вызвал промпт. Если хоть одно условие не выполнено, план паркуется как cli_contract — ещё до того, как дирижёр посмотрит на заявления сессии. CLI, который перестал загружать хуки или навыки, ловится на первой же сессии, какую бы версию он ни показывал.
Что нашёл зонд и чего никто не предполагал
Зонд окупился ещё дважды.
В первый раз дело было в каталоге. Рецензия при закрытии раз за разом оставляла замечание открытым с пометкой «эта сессия не может править .claude/». Никакого такого правила не было; одно из прежних решений сочло это вопросом о том, кому в дорожке что принадлежит, и разрешило закрытию править файлы навыков. Замечание вернулось. ADR-0210 закрыл вопрос тем, что спросил сам CLI, а не стал снова гадать. Зонд запустил две сессии: одну ровно с настройками дирижёра, другую — с правилом разрешения для .claude/ во всех возможных написаниях. В обеих читать файлы там можно, а править и записывать — нельзя: «Permission to use Edit has been denied because Claude Code is running in don’t ask mode». Запись вне .claude/ — в том же worktree, в том же ходе — прошла.
Это CLI защищает каталог конфигурации проекта, а не дыра в списке разрешений. Пока зонд это не показал, неверная трактовка успела обойтись трижды: открытое замечание, которое никуда не передали; фаза, которая сделала всю работу и запарковалась на собственном критерии готовности; и исправление, которое я закоммитил вручную. Теперь правило такое: фаза, в файлах которой есть .claude/, паркуется до запуска, а в подробностях парковки лежит точная правка. Дирижёр перестал обещать то, что запрещает инструмент под ним.
Вторая находка была неприятнее. Список разрешений сессии разрешает rm целиком, а ограничивают его запреты на четыре способа, которыми записанный путь может выйти за пределы worktree: .., ~, ведущий / и буква диска. В README дирижёра гарантия звучала так: «путь за пределами дорожки отклоняется, для чего бы он ни был». ADR-0233 фиксирует, что правила обеспечивают совсем не эту границу. Шаблон по тексту команды не может ограничить путь, которого не существует, пока оболочка его не раскроет. rm -rf $HOME/.cargo не попадает ни под один запрет.
Тест, который проверял эту гарантию, тоже этого не видел, и его собственный заголовок честно предупреждал: «Это модель CLI, а не CLI». Модель разрезала составные команды по && и ; и утверждала, что cd studio && npm run typecheck запрещена. Настоящая сессия на настоящем CLI с ровно этим файлом настроек сделала 26 вызовов оболочки с cd. Двадцать два из них выполнились.
Правило, которое из этого вышло, я бы взял в любой проект со списком разрешений:
Утверждение, что список разрешений что-то запрещает, проверяется по записанному транскрипту настоящего CLI; утверждение, что он что-то разрешает, может проверяться и на модели.
Ошибка в разрешающем случае стоит сессии одного хода и видна в журнале. Ошибка в запрещающем ничем не ограничена и происходит молча. Платить за доказательства стоит там, где цена ошибки не ограничена, и больше нигде. Это решение всё ещё в статусе proposed, а план, который его реализует, не закрыт. Пока он не закрыт, фраза в README — утверждение, которое, как я знаю, сильнее своих доказательств, и здесь я об этом прямо говорю.
Дирижёр верит репозиторию, а не сессии
Блок итога — это заявление. Дирижёр сверяет его с git: коммиты существуют и лежат на ветке, строки ## Implementation log в плане ссылаются именно на эти коммиты, рабочее дерево чистое, закрытие перенесло план в done/ и поставило аннотированный тег на вершину ветки. Если git заявление не подтверждает, план паркуется как disagreement.
В журнале прогонов три таких расхождения, и ни в одном модель не врёт. Каждый раз она права лишь приблизительно — и эта приблизительность оказывается важной.
- Закрытие сообщило, что замечание исправлено в коммите, который не трогал файл, к которому относилось замечание. Скорее всего, исправление было настоящим и лежало в другом месте, но запись навсегда указывала бы не туда.
- Закрытие записало строку
Status:плана целым абзацем: «done. Phases 14ae5f69, 776b946f, e3184783, 1eb144fa, and close repairs 80bf58cb. The conductor-run review found no blockers…» Верно, информативно — и совсем не то одно слово, которое ожидает любая программа, читающая эту строку. - В журнале сессии реализации строка для фазы 1 ссылалась на коммит, сделанный на более раннем шаге, а не на этом. История верная, авторство — нет, а именно по авторству следующее возобновление решает, с чего начинать.
Ни одна из этих ошибок не уронила бы тест. Каждая оставила бы запись, которая выглядит правильно для человека, пробегающего её глазами, и неправильно — для программы, которая прочтёт её следующей. Такие ошибки самоотчёт сессии поймать не может: его пишет та же сессия, что их допустила.
Дыра в исходном замысле
Первое же закрытие дирижёра нашло дыру именно в этом принципе, и показательно, что она там оказалась.
В первой версии собственная проверка дирижёра шла до рецензии, а при перемотке вершина, оставленная сессией закрытия, считалась уже проверенной. Но сессия закрытия вливает main, повышает версию и ставит тег. Значит, дерево, попадавшее в main, — а при двух дорожках это первое дерево с кодом обеих — было проверено лишь заявлением самой сессии. А это ровно то, чего, согласно ADR, дирижёр не принимает никогда. Исправление: перемотку сверяют с вершиной, на которой последний раз прошла проверка самого дирижёра, так что каждая вершина закрытия проверяется до того, как сдвинется main. Цена — одна лишняя проверка на каждое закрытие.
ADR честно признаёт и неудобный факт. Рецензию при закрытии самого дирижёра проводил не дирижёр. Его строили в сессиях, запущенных вручную, а закрывала его ручная сессия architect — она же и написала это исправление. Так что какое-то время единственным исправлением, которое больше никто не рецензировал, была та самая проверка, через которую проходило всё последующее.
Парковка — ответ по умолчанию
Самая важная фраза ADR — его заголовок: любое решение, которое дирижёр не может принять сам, паркует план. Список причин для парковки закрыт. human-фаза. Условие остановки, прописанное в самом плане. Сессия, которая обнаружила, что план неверен, или которой нужен ответ на вопрос. Красная проверка. Рецензия, которая не проходит после двух раундов исправлений. Исчерпанный лимит трат. Расхождение. Запаркованный план сохраняет свой worktree и ветку и оставляет во входящих запись с файлом, который нужно прочитать, и командой для возобновления, а дорожка переходит к следующему плану в очереди, чьи зависимости уже влиты.
Промпт каждой сессии реализации говорит то же самое с другой стороны: остановись и запаркуйся, а не обходи препятствие, если встретишь human-фазу, условие остановки из плана, неверный план, вопрос, на который может ответить только человек, или проверку, которую не удаётся сделать зелёной в рамках фазы. А в первом абзаце сказано: «Этот разговор никто не прочтёт, и на вопросы никто не ответит». Эта фраза влияет на поведение сильнее, чем всё остальное в шаблоне. Сессия, которая думает, что ей могут ответить, задаёт вопрос. Сессия, которая знает, что никто не ответит, либо паркуется, либо импровизирует, и весь промпт построен так, чтобы парковка обходилась дешевле.
Первый прогон не влил ни одного плана
Первый живой прогон вечером 14 сентября длился 73 минуты, не влил ни одного плана и запарковал три — на $25 условных трат. Со стороны это провал. По журналу — самый полезный прогон за всю историю дирижёра.
Две парковки из трёх — это замысел, который сработал. Две сессии реализации нашли каждая настоящий дефект в своём плане, записали его в журнал плана и остановились. Это было первое свидетельство против риска, которого я боялся больше всего: что сессия начнёт импровизировать вокруг сломанного плана вместо того, чтобы запарковаться. Два случая — ещё не статистика, и в журнале так и записано.
Третья парковка и одна молчаливая остановка оказались дефектами самого дирижёра, и каждый подтвердился по коду:
- Дирижёр записывал pid-файл раньше, чем кто-либо создавал каталог состояния, поэтому первый
runна чистом чекауте падал. Тесты этого не ловили: их фикстура создавала каталог заранее. - Проверка перед рецензией запускала тест, который исправление из самого плана как раз и должно было сломать: план закрывал запись в бэклоге тем, что проверка этой записи начинала падать. Проверка запарковала план раньше, чем до него дошла рецензия, при закрытии которой запись ушла бы в архив. Проверка выносила суждение, которое принадлежит рецензенту, раньше, чем рецензент успевал его вынести.
- Список разрешений запрещал
git restore. Сессия, которой велено оставить дерево чистым, не могла откатить файлы, перезаписанные её же прогоном тестов, и запарковалась с восемью перекодированными эталонными изображениями в дорожке. - Дорожка молча останавливалась, упираясь в лимит worktree. Три парковки держали три worktree, поэтому четвёртый план так и не стартовал, и ни вывод, ни сводка не объяснили почему.
На следующее утро, после плана, который исправил все четыре дефекта, пилот проработал 3 ч 38 мин и влил три плана без единой парковки и без единого раунда исправлений — на $55 условных. Каждый шаг, до которого не дошёл первый прогон, — рецензия, закрытие, блокировка закрытия, проверка вершины закрытия, тег, перемотка, удаление дорожки — отработал от начала до конца.
Что парковки говорят о планах
С 22 сентября в журнале 17 планов: 13 влиты, 3 запаркованы, 1 в очереди. Парковки распределились так:
| Причина | Парковок |
|---|---|
plan_wrong | 8 |
human_phase | 8 |
disagreement | 3 |
check_red | 3 |
gate_red | 2 |
stop_condition | 2 |
merge_conflict | 1 |
claude_dir | 1 |
api | 1 |
Больше всего меня удивила первая строка. Самая частая причина парковки — неверный план, а планы мои: написаны вместе с сессией architect и одобрены мной. Фаза, у которой What и Done when описывают разные этапы. Фаза, которой нужно менять код за пределами заявленных в ней файлов. Критерий готовности, требующий снимка экрана, который может сделать только оконное приложение на реальном дисплее, — выданный headless-сессии, у которой нет ни того, ни другого. Фаза, требующая внести документ в список проверки, из которого проверка его намеренно исключает.
Когда план читает человек, ничего из этого не видно. Всё это видно тому, кто пытается выполнить план буквально. Это самое полезное, что дирижёр рассказал мне о моих же планах. Формат планов создавался для сессии под присмотром человека, а такая сессия сглаживает именно эти нестыковки, даже не замечая этого. Буквальный исполнитель ничего не сглаживает.
Сначала починить, потом парковать
Через двенадцать дней журнал показал обратную проблему. ADR-0248 начинается с разбора 22–24 сентября: десять парковок на девять планов, и из 42 прошедших часов машина работала около 8. Всё остальное время она ждала меня. А две причины из закрытого списка оказались вовсе не теми решениями, для которых нужен человек.
Красная проверка — обычно сбой, который сессия способна починить, а чинящие сессии у дирижёра уже есть: раунды исправлений после рецензии. Один план покраснел на единственном тесте после моего же ручного коммита для его human-фазы и час с четвертью ждал исправления, которое могла бы написать сессия dev.
Конфликт слияния — то же самое. Рецензия вернулась чистой — ни блокеров, ни мажоров, — а git merge main при закрытии дал конфликт в двух документах и двух исходниках. Я разрешил его вручную за 11 минут. После этого resume перезапустил всю рецензию с первого раунда — это стоило $15.29 и 29 минут, — потому что запаркованная сессия рецензии не сохраняет вердикт. Чистый вердикт пропал вместе с парковкой.
Неверный план был худшим случаем: один план запарковался как plan_wrong после $55.94 трат на реализацию — из-за противоречия между двумя полями одной фазы, которое можно было заметить до первой строки кода.
ADR внёс пять изменений, и все небольшие.
- Перед первой сессией реализации идёт проверка готовности. Это свежая сессия
architectв режиме только для чтения: она сверяет What, Files touched и Done when каждой фазы между собой и с деревом и проверяет, что каждый критерий готовности можно выполнить в рамках списка разрешений. Оценивает она согласованность, а не архитектуру. Итог —readyили парковкаplan_wrongс указанием фазы, ещё до каких-либо трат на реализацию. - Дорожка сама вливает
mainперед проверкой до рецензии, чтобы конфликты всплывали, пока план ещё у реализатора. - На конфликт выделяется одна сессия слияния — свежий
dev, которому передают список конфликтующих путей. Сессия закрытия никогда не разрешает конфликты в коде: закрытие ведётarchitect, а архитектор код не пишет. - На красную проверку выделяется одна сессия починки на этап, не больше трёх на план. Промпт починки запрещает менять утверждение, эталон или входные данные теста, чтобы он прошёл; если сессия считает, что неверен сам тест, она паркуется как
plan_wrong. - Рецензия и закрытие — разные сессии, и чистый вердикт переживает парковку. Он остаётся в силе, пока каждый коммит после проверенной им вершины — это либо слияние
main, либо коммит сессии закрытия, слияния или починки. Любой другой коммит — включая мою собственную ручную правку — запускает новую рецензию, потому что его никто не рецензировал.
Последнее условие нравится мне больше всего. Было бы легко сделать исключение для коммитов владельца. Но коммит, который я сделал руками в одиннадцать вечера, — как раз тот, что проверяли меньше всего, и правилу всё равно, кто я такой.
С тех пор проверка готовности запускалась четырнадцать раз, в сумме на $9, и половина из восьми парковок plan_wrong в таблице выше пришлась на неё — до того, как реализатор что-либо потратил. Минус я скрывать не буду: это модель, которая проверяет на согласованность план, написанный с помощью модели, и нестыковка, которую она пропустит, всё равно дойдёт до реализации. Но при цене меньше доллара за проверку это с большим отрывом самая выгодная сессия в конвейере.
Остановиться труднее, чем начать
Первую версию можно было остановить только одним способом: abort, который убивает все сессии дирижёра, так что прерванный шаг в следующий раз начнётся заново, а потраченное пропадёт. Для «стоп, немедленно» это правильно. Но мне это почти никогда не было нужно. Мне нужно было «доделай текущее и остановись»: машина понадобилась для другого, human-фаза готова, а GPU занят, или просто рабочий день закончился.
Вручную это значит следить за журналом и ловить момент для abort, причём оба варианта провала лежат по разные стороны одного и того же момента. Прервёшь рано — пропадут траты на рецензию; поздно — уже стартовала сессия следующего плана. В ADR-0219 записано, что за один день так приходилось делать трижды — однажды опрашивая файл состояния в цикле, пока план не станет merged, и прерывая в образовавшемся зазоре.
Решение — команда pause, и две её детали ценнее самой команды.
Просьба — это состояние, которое опрашивает работающий процесс, а не сигнал. На Windows сигнал вообще не может вызвать обработчик дирижёра — именно поэтому abort убивает всё дерево процессов. Так что pause просто пишет файл, который цикл дорожки и так проверяет между планами.
Просьба живёт не дольше своего прогона. Если бы пауза сохранялась, утренний run превращался бы в процесс, который запускается, ничего не делает и завершается, — со стороны это неотличимо от зависания и становится ловушкой для единственного оператора этого инструмента. Поэтому пауза снимается, когда заканчивается её прогон, а новый прогон, обнаружив паузу, оставшуюся от упавшего дирижёра, снимает её и сообщает об этом. Сказать «завтра ничего не запускай» поэтому нельзя. Если ничего не нужно — просто не запускай прогон.
Кроме того, pause выводит, чего он теперь ждёт: какой план, какой шаг и сколько времени уже на нём. Между просьбой и остановкой может пройти десять минут — столько идут тесты, — и оператор, который этого не видит, всё равно потянется к abort.
А потом он перестал останавливаться
Тот же разбор, из которого выросло правило про починку, показал, что с 22 по 24 сентября я запускал дирижёр десять раз. Самый длинный прогон шёл 2,4 часа, а все вместе — около 8 часов из 42. Прогон завершался, как только ни одна дорожка не могла двигаться дальше, и после каждой парковки human_phase мне нужно было выполнить фазу, отметить её строку, а потом отдельно набрать resume и run. Во втором шаге никакого решения не было: resume и так отказывался работать, пока строка не отмечена.
ADR-0250 сделал run резидентным. Простаивающая дорожка засыпает на минуту и проверяет снова, так что план, одобренный и поставленный в очередь во время прогона, стартует в течение минуты. Лимит worktree теперь означает ожидание, а не остановку. А по закрытому списку причин план теперь возобновляется сам, когда по дереву видно, что вопрос решён: human-фаза — когда её строка в журнале стала done, лимит использования — когда прошло записанное время сброса, грязный основной чекаут — когда он снова чист. Поверх грязного worktree ничего не возобновляется никогда. Все остальные причины остаются за мной, потому что ни по одной из них дерево не может показать, что вопрос решён.
Резидентный режим значит, что деньги тратятся, пока никто не смотрит, поэтому у него свой потолок. Когда достигнут run_budget_usd, прогон встаёт на паузу: планы в работе доводятся до конца, новые не начинаются. А поскольку резидентный прогон может пережить обновление CLI, версию теперь проверяют перед каждой сессией, а не раз за прогон.
Ещё одна деталь, которая мне нравится: при запуске дирижёр хеширует собственные исходники и при каждой проверке сравнивает хеши. Если, пока он работает, влито изменение в сам дирижёр — а он действительно вливает изменения в самого себя, потому что планы, меняющие дирижёр, ведёт дирижёр, — он выводит одну строку со списком изменённых файлов и встаёт на паузу. Он никогда не перезапускает себя и никогда не прерывает сессию. Программа, способная вливать изменения в саму себя, должна хотя бы замечать, что она это сделала.
Во что обошлись двенадцать дней
Журнал прогонов охватывает 22–26 сентября: 21 прогон, 78 headless-сессий, 5 082 хода, $383. Доллары — это то, что CLI сообщает как стоимость каждой сессии, но дирижёр работает по подписке, так что суммы условные. Это полезная относительная мера, а не счёт. На деле ограничивают окна использования аккаунта — пятичасовое и семидневное, — поэтому живой вывод показывает каждое их изменение, а сводка фиксирует их в начале и в конце каждого прогона. В первый день, пока работал дирижёр, семидневное окно было заполнено на 0,85. По долларам об этом ничего нельзя было понять.
Куда ушли $383:
| Шаг | Сессий | Условные траты |
|---|---|---|
| реализация | 30 | $261 |
| рецензия | 18 | $96 |
| закрытие | 7 | $11 |
| проверка готовности | 14 | $9 |
| слияние | 5 | $3 |
| исправление | 3 | $2 |
| починка | 1 | меньше $1 |
Реализация — две трети, как и должно быть. Рецензия — четверть: такова цена рецензента, который перечитывает всё без краткого содержания. Всё, что добавили более поздние решения, — готовность, слияние, починка — вместе меньше 4 %.
Треть времени ушла на то, чтобы заново доказать, что дерево зелёное
Другая находка пилота касалась не денег, а времени. Из его 218 минут 72 заняла собственная проверка дирижёра, в основном полный прогон тестов: полный cargo nextest run --workspace держит блокировку тестов от 10,4 до 11 минут, и на трёх планах таких прогонов было тринадцать.
План без раундов исправлений прогонял тесты до пяти раз — в конце работы реализатора, на проверке до рецензии, внутри рецензии, при закрытии и на проверке после закрытия, — причём две пары из них шли на побайтно одинаковых деревьях. У каждого повтора была одна и та же веская причина: заявление сессии, что тесты прошли, — не доказательство. Рецензия перезапускала тесты, потому что «тесты прошли» от реализатора — это просто текст. Дирижёр перезапускал, потому что у рецензии это тоже просто текст.
Но когда тесты идут через обёртку блокировки дирижёра, код возврата видит процесс дирижёра, а не модель. Доказательство уже было — его просто никто не сохранял. ADR-0207 его сохраняет: это реестр, где на каждый полный прогон, который наблюдал код дирижёра, есть строка с хешем дерева, кодом возврата и итоговой строкой вывода. Прогон записывается, только если worktree был чистым и в начале, и в конце, так что хеш указывает ровно на то, что проверялось. Следующая проверка на том же дереве находит запись, выводит её и пропускает прогон.
Перенимать здесь стоит то, от чего отказались. Очевидное более узкое правило — «пропускать, если изменилась только документация», и для этого репозитория оно неверно: тесты на Rust читают из docs/ пять разных вещей. Классификатору путей понадобилась бы защита, привязывающая его к тому, что читает каждый тест, и он всё равно остался бы эвристикой. Поэтому совпадение определяется идентичностью дерева, а не суждением о том, что изменилось. Экономия берётся из того, что работу переставили так, чтобы одинаковые деревья действительно возникали, а не из решения, что какая-то разница неважна. Реализатор больше не гоняет полный набор тестов в конце, потому что сразу за ним на том же коде его прогонит проверка до рецензии; закрытие запускает свою проверку последней, на той вершине, которую будет тегировать. Теперь план без раундов исправлений прогоняет полный набор тестов дважды.
В том же пилоте был урок поменьше — про измерительные приборы. Сводка показывала пятнадцать минут ожидания блокировки тестов, и это выглядело как борьба двух дорожек за ресурс. Но дорожка была одна. Все четырнадцать минут приходились на три вызова cargo nextest list — запросы метаданных, которые не запускают ни одного теста, — стоявшие в очереди за полными прогонами. Блокировка не отличала перечисление тестов от их запуска, и число означало совсем не то, что показывало.
Страница, которую я читаю по утрам
Всё, что дирижёр делает в моё отсутствие, сводится на одну страницу, digest.md, которая пересобирается из состояния и git после каждого шага. После нескольких итераций в ней осталось два раздела и больше ничего.
Needs you идёт первым и представляет собой весь мой список дел: каждая текущая парковка с её возрастом, занятым worktree и командой возобновления; каждая дорожка, упёршаяся в лимит worktree; каждый коммит починки, попавший в main без рецензии, — по SHA, пока он не окажется в origin; открытые замечания рецензии по каждому влитому плану с file:line. Раздел начинается со счётчика, так что пустой первый раздел означает, что меня ничего не ждёт.
Now — по дорожкам: план, шаг, сколько времени на нём и сколько потрачено.
Что произошло за ночь, на этой странице намеренно нет. Для этого есть отдельная страница истории, которая генерируется по запросу, или закоммиченный раздел ## Close review в закрытом плане. В первой версии одно смешивалось с другим, и страница текущего состояния постепенно превратилась в журнал, который приходилось читать снизу вверх.
С замечаниями рецензии то же самое. Замечания уровня minor и nit, которые закрытие не исправило, — мои, и они висят на странице, пока я не скажу, что стало с каждым: --done, --wontfix или --filed, причём причина обязательна. Пустую причину команда не принимает. Решение по замечанию ничем не проверить — в отличие от fixed_in, который указывает на коммит и сверяется дирижёром с веткой, это суждение, которое не с чем сверить, — поэтому фраза, которую я набираю, и есть вся запись о нём. Ни сессия, ни закрытие, ни проверка написать её не могут. Правило маленькое, но именно оно не даёт странице превратиться в список, который я привык не замечать.
От чего я отказался
ADR перечисляет свои минусы, и никуда они не делись.
Я больше не читаю вердикт до того, как по нему начинают действовать. После чистой рецензии план закрывается и вливается, а саму рецензию никто не читает. Страховка — в устройстве процесса: действует только вердикт без блокеров и мажоров, рецензия коммитится вместе с планом, и ничего не уходит с машины, пока я не запушу. Но закрытие, которое я бы оспорил, может успеть попасть в main. Откатывается оно новым коммитом, а не переписыванием истории.
Цикл исправлений — это модель, которая оценивает модель, причём дважды. Два раунда — это потолок, а не гарантия. Рецензент, который дважды ошибается одинаково, протащит свою ошибку. С ручным закрытием было так же. Просто теперь дирижёр делает это без моего взгляда на строку с вердиктом.
Две дорожки — это не вдвое больше, чем одна. По цифрам пилота вторая дорожка могла работать одновременно с первой только в той части плана, где не нужна блокировка тестов, а в остальное время стояла бы за ней в очереди. ADR тогда говорил прямо: это решение против параллелизма, принятое по результатам измерений, и пересматривать его стоит, только когда тесты перестанут заново гоняться на зелёных деревьях. Вторую дорожку включили после появления реестра, и сейчас работают обе. Но честный вывод из июльского поста по-прежнему верен, только в новой форме: машина даёт меньше параллелизма, чем кажется, и его ограничивает самый последовательный ресурс. Раньше этим ресурсом был я. Теперь — GPU-тесты.
Это инструмент третьего рода. Дирижёр — не проверка и не рендерер. Это программа, которая запускает другие программы и тратит деньги, и она дрейфует вместе с CLI, который обновляется сам. У неё есть свои тесты — на поддельном CLI, который воспроизводит записанные потоки событий, — и эти тесты являются моделью CLI ровно в том же смысле, что и тест списка разрешений. Сверяет их с настоящим CLI только зонд.
Где теперь человек
Список того, что остаётся ручным, настолько короткий, что его можно привести целиком: одобрение плана, каждая human-фаза, каждая парковка, которую нельзя разрешить по дереву, и каждый пуш.
Это не «человек вне петли». Это человек в четырёх точках, и в каждой из них он принимает решение, — вместо восьми, половина из которых сводилась к набору текста. Одобрение — место, где я решаю, чему быть. human-фаза — где не обойтись без физической машины и моих глаз. Парковка — где план расходится с реальностью и кто-то должен решить, кто из них неправ. Пуш — где работа покидает машину и становится публичной. Всё, что между этими точками, берёт на себя дирижёр — и, на это и ушли двенадцать дней, справляется с этим, отказываясь решать то, чего не может проверить.
В июле ограничением было моё внимание, и я прямо об этом сказал. Оно им и осталось. Изменилось то, на что оно уходит. Уже не на открытие worktree и набор «поехали», а на чтение планов, которые дирижёр счёл неверными, — и, как выяснилось, это самое информативное, что он производит.