Одобрение — это и есть «поехали»
В июле я написал, что параллельные агентные сессии дали меньше, чем я ждал, и что дело было не в агентах: ограничением всегда были не возможности агентов, а мои. Одну хорошо управляемую петлю я запускал куда чаще, чем несколько, а параллельную машинерию держал как доступную, а не как режим по умолчанию.
Две недели назад я закончил пост про обвязку направлением, в которое указывало каждое изменение: убирать человека из петли везде, где он присутствовал только потому, что что-то не было записано или не было сделано исполнимым.
Этот пост — про самый крупный шаг в этом направлении. У Ritmolux теперь есть дирижёр (conductor): программа на Node, которая берёт одобренные планы из очереди и доводит каждый до смёрженного main без единого действия владельца между ними. Он открывает дорожку в git worktree, запускает свежие headless-сессии claude -p, чтобы реализовать фазы плана, сверяет каждое их утверждение с git, гоняет собственную проверку, запускает отдельного рецензента и отдельного закрывающего, перематывает main вперёд и удаляет дорожку. Всё, чего решить не может, он паркует, и дорожка переходит к следующему плану.
Он никогда не пушит. Всё, что он делает, остаётся на машине, пока я не прочитаю, что произошло, и не запушу сам.
Ему двенадцать дней. За это время в него ушло 130 коммитов, около 5 700 строк Node и шаблонов промптов и порядка двадцати записей о решениях, большая часть которых поправляет первую. В этом соотношении вся история. Первый замысел был в основном верным, и почти каждая поправка пришла из прогона, который показал, где он неверен.
Четыре вмешательства без единого решения
Запись, с которой всё началось, ADR-0205, открывается описью. Каждый план стоил мне четырёх вмешательств: открыть дорожку в worktree; набрать «поехали» сессии dev, чей план я уже одобрил; запустить свежую сессию /architect, когда dev напечатает указатель на закрытие; перемотать main и удалить дорожку, когда закрытие закончится.
Ни одно из четырёх не содержало решения. 14 сентября я одобрил одиннадцать планов за один присест, и при таком темпе ход задавали вмешательства, а не работа. Одиннадцать планов по четыре шва — это сорок четыре раза, когда мне надо было оказаться у клавиатуры в нужный момент ради действий, содержание которых уже было решено.
Но каждый из этих швов был в своё время обоснован письменно, более ранним решением. Поэтому первой задачей было ничего не строить. Нужно было отделить каждый довод от механизма, который его случайно нёс, и посмотреть, переживёт ли довод исчезновение механизма.
«“Поехали” — это одобрение». Верно, но одобрение, которое фиксирует «поехали», уже состоялось: план запускается, только когда в его файле написано Status: approved. Второе подтверждение страховало от другого риска — сессии, которая работает за пределами своего объёма. Этот риск так же хорошо закрывает сессия, которой выдан точный диапазон фаз и велено остановиться на его конце. Поэтому для плана, который ведёт дирижёр, одобрение и есть «поехали». Вне дирижёра сессия dev, запущенная человеком, по-прежнему пересказывает план и ждёт.
«Рецензия на закрытии ничего не стоит изнутри сессии, которая писала код». Тоже верно, и главное слово тут — изнутри. Свежесть — свойство контекста рецензента, а не того, кто открыл сессию. Рецензент, запущенный отдельным процессом и получивший только путь к плану и дорожку, не несёт в себе никаких рассуждений реализации — ровно то же, что несёт свежая ручная сессия. И он получает то, чего у ручного шва никогда не было. Когда рецензента запускал я, он был свежим ровно настолько, насколько я удерживался от того, чтобы вставить ему краткое содержание. Дирижёр не может передать рецензенту краткое содержание: у него его нет.
«human-фазу некому передать». Верно, и это не изменилось. Фаза, где нужен я у эталонной машины или у монитора, чтобы оценить картинку, по-прежнему требует меня.
Два довода из трёх пережили исчезновение механизма. В этом весь аргумент за дирижёра, и его стоило записать до первой строки кода, потому что это заодно и список того, что дирижёр не должен незаметно подточить.
Альтернативы, и почему каждая переносила вмешательство, а не убирала его
В записи семь альтернатив. Три стоит повторить: каждая выглядит очевидным ходом, и каждая проваливается на факте, а не на вкусе.
Скрипт workflow внутри сессии родной для инструмента и пишется быстрее всего. Он проваливается потому, что workflow заканчивается вместе с сессией Claude Code, которая его запустила, и возобновляется только в той же сессии. Очереди, которая тянется днями и паркуется в ожидании человека, понадобилось бы, чтобы я держал сессию живой. Вмешательство переезжает, а не исчезает.
Субагенты с изоляцией в worktree проваливаются на двух фактах. Агенту в изолированном worktree запрещены операции git над основным чекаутом, так что смёржить он не может. А субагент живёт внутри породившей его сессии — снова проблема workflow.
Agent SDK в роли движка даёт типизированный API сессий и колбэк разрешений на каждый вызов. От него пока отказались: он добавляет пакет в репозиторий, который считает каждую зависимость, а тот же контроль достижим через claude -p, файл настроек и механизм хуков, который в проекте уже есть. Запись называет условие пересмотра: если разбор потока событий 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 её ослабил: версия с теми же мажором и минором, что у проверенной, но с большим патчем, работает с предупреждением, которое висит на странице статуса, пока кто-нибудь её не проверит. Цена названа честно, и это я бы скопировал: этот CLI нумерует почти каждый релиз как патч, так что правило превращает защиту в предупреждение почти при каждом обновлении. И риск уже был зафиксирован ровно там: между этими двумя патчами показание лимитов использования переехало в другое поле.
Так что полезный вопрос был не «проверена ли версия», а какое тихое изменение прошло бы незамеченным. Большинство — нет. Пропавшее событие результата паркует план, пропавший блок итога паркует, кривой — тоже: читатель потока уже отказывает закрыто. Опасный класс — изменение, которое отказывает открыто, и такое есть одно: хуки проекта не запустились. Это замок набора тестов, запрет пуша, запрет массового добавления в индекс и запрет подписи. Сессия без них всё равно заканчивается корректным итогом, и ничто из того, что читал дирижёр, этого бы не показало.
Поэтому теперь каждая сессия дирижёра обязана доказать, что её хуки работали. Хук замка тестов дописывает строку в журнал шага, путь к которому дирижёр передаёт в окружении. Когда сессия заканчивается, рядом с транскриптом, где есть хоть один вызов оболочки, обязан лежать журнал хука, а событие инициализации потока обязано перечислять навык, который вызвал промпт. Любой из двух провалов паркует план как 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 — при двух дорожках первое дерево с кодом обеих, — было проверено только утверждением самой сессии. А это ровно то единственное, чего, по решению, дирижёр никогда не принимает. Исправление: перемотку сравнивают с вершиной, на которой последний раз прошла собственная проверка дирижёра, так что каждая вершина закрытия проверяется до того, как сдвинется main. Цена — ещё одна проверка на каждое закрытие.
Запись честна и насчёт неловкого факта. Рецензию на закрытии самого дирижёра проводил не дирижёр. Его строили в сессиях, запущенных человеком, и закрывала его запущенная человеком сессия architect, которая заодно и написала это исправление. Так что какое-то время единственным исправлением, которого никто больше не рецензировал, была проверка всего, что шло после него.
Парковка — ответ по умолчанию
Самая важная фраза решения — его заголовок: любое суждение, которое дирижёр не может вынести, паркует план. Список причин парковки закрыт. 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 сентября: десять парковок на девять планов, и машина работала примерно 8 часов из прошедших 42. Остальное время она ждала меня. И две причины из закрытого списка оказались вовсе не суждениями.
Красная проверка обычно — сбой, который сессия может починить, а дирижёр уже запускает чинящие сессии: раунды исправлений после рецензии. Один план покраснел на единственном тесте после моего же ручного коммита для его human-фазы и час с четвертью ждал исправления, которое могла написать сессия dev.
Конфликт мёржа — то же самое. Рецензия вернулась чистой — ни блокеров, ни мажоров, — а git merge main при закрытии дал конфликт в двух документах и двух исходниках. Я разрешил его руками за 11 минут. После этого resume перезапустил всю рецензию с первого раунда, что стоило $15.29 и 29 минут, потому что запаркованная сессия рецензии не записывает вердикт. Чистый вердикт пропал вместе с парковкой.
Неверный план был худшим случаем: один запарковался как plan_wrong после $55.94 реализации — на противоречии между двумя полями одной фазы, которое можно было прочесть до первой строки кода.
Решение внесло пять изменений, и все небольшие.
- Перед первой сессией реализации идёт проверка готовности. Это свежая сессия
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, это не коммит, который дирижёр сверяет с веткой, а суждение, сверенное ни с чем, — так что фраза, которую я набираю, и есть вся запись о нём. Ни сессия, ни закрытие, ни проверка написать его не могут. Правило маленькое, и именно оно не даёт странице стать списком, который я научился не замечать.
От чего я отказался
Запись о решении перечисляет свои минусы, и никуда они не делись.
Я больше не читаю вердикт до того, как по нему действуют. Чистая рецензия закрывает и мёржит непрочитанной. Смягчение структурное: действует только вердикт без блокеров и мажоров, рецензия коммитится вместе с планом, и ничто не уходит с машины, пока я не запушу. Но закрытие, которое я бы оспорил, может дойти до main раньше. Откат — это новый коммит, никогда не переписывание истории.
Цикл исправлений — это модель, оценивающая модель, дважды. Два раунда — потолок, а не гарантия. Рецензент, дважды ошибающийся одинаково, протащит свою ошибку. Так было и с ручным закрытием. Дирижёр просто делает это без моего взгляда на строку вердикта.
Две дорожки — это не вдвое больше одной. По цифрам пилота вторая дорожка могла перекрываться только с той частью плана, которая не держит замок набора, а в остальном стояла бы в очереди за первой. Запись тогда говорила прямо: это решение против параллелизма по измеренным причинам, и пересматривать его — только когда набор перестанет перезапускаться на зелёных деревьях. Вторую дорожку включили после того, как появился реестр, и сейчас работают обе. Но честный итог июльского утверждения по-прежнему верен в новой форме: параллелизм, который предлагает машина, меньше, чем кажется, и ограничен самым сериализованным ресурсом. Раньше этим ресурсом был я. Теперь — набор тестов на GPU.
Это третий вид инструмента. Дирижёр — не проверка и не рендерер. Это программа, которая запускает другие программы и тратит деньги, и она дрейфует вместе с CLI, который обновляется сам. У неё свои тесты — против поддельного CLI, воспроизводящего записанные потоки событий, — и эти тесты являются моделью CLI ровно в том смысле, в каком ею был тест списка разрешений. Сверяет их с настоящим только зонд.
Где теперь человек
Список того, что остаётся ручным, достаточно короток, чтобы привести его целиком: одобрение плана, каждая human-фаза, каждая парковка, которую дерево не может уладить, и каждый пуш.
Это не «человек вне петли». Это человек в четырёх точках, каждая из которых — суждение, вместо восьми, половина которых была набором текста. Одобрение — место, где я решаю, чему быть. human-фаза — где справятся только физическая машина и мои глаза. Парковка — где план и мир расходятся и кто-то должен решить, что из них неверно. Пуш — где работа покидает машину и становится публичной. Всё между этими точками берёт на себя дирижёр, и — на это и ушли двенадцать дней — берёт тем, что отказывается решать что-либо, чего не может проверить.
В июле ограничением было моё внимание, и я так и сказал. Оно им и осталось. Изменилось то, куда оно уходит. Оно больше не уходит на открытие worktree и набор «поехали». Оно уходит на чтение планов, которые дирижёр счёл неверными, — а это, как выяснилось, самое информативное, что он производит.