Вчера в 19:17 я начал набрасывать план для бота расходов: сворачивать длинные списки в чате и показывать позиции чеков по категориям. В 19:20 одобрил его и поставил в очередь. В 21:19 он был влит в main — с тестами, с рецензией от сессии, которая кода в глаза не видела, и с новой версией. В 21:24 он уже ехал в прод. Я не написал в нём ни строчки и не смотрел, как его пишут.

Это был один из шестнадцати планов, которые 6 октября дошли до main. В графике активности на GitHub появилась клетка, какой там ещё не было: 207 коммитов за день, 113 в personal_expenses_bot, 93 в Ritmolux и один в README профиля.

Хочу написать про этот день, потому что лучше всего он показывает, какую скорость сейчас дают агенты. Но «207 коммитов» — плохая мера, и кто видит такое число, обычно думает одно из двух: накрутка или модель, которая очень быстро строчит код. Ни то ни другое, и когда я разобрал день по частям, сам удивился. Больше трети этих коммитов — бумажная работа. А упёрлась скорость не в модель и, по большому счёту, даже не в машины.

И ещё одно сразу, иначе число читается неправильно: ночью ничего не работало. Ноутбук уснул накануне в 22:29 и проснулся в 08:05. Всё, о чём этот пост, случилось между восемью утра и десятью вечера. Длинный рабочий день, а не сутки.

Пара слов из прошлого поста

Это продолжение поста «Одобрение — это и есть „поехали“» — про дирижёра. Если коротко, это программа на Node: она берёт одобренные мной планы из очереди и доводит каждый до слияния в main через свежие headless-сессии claude -p. За этими сессиями никто не смотрит. 6 октября работали сразу два дирижёра, по одному на проект, а я в основном стоял между ними.

Несколько слов из того поста здесь будут встречаться часто, поэтому по-простому:

  • Дорожка — отдельный git worktree, в котором идёт один план. В каждом проекте их было две.
  • Фаза — один шаг плана, обычно один коммит. План проходит проверку готовности (не противоречит ли он сам себе), реализацию по фазам, прогон тестов (полный набор, который дирижёр запускает сам), рецензию отдельной сессией и закрытие, где повышается версия и план вливается.
  • Запарковать — остановиться и ждать меня. Всё, что дирижёр не может решить сам, он паркует.
  • Отложенная фаза — шаг, которому нужен человек, например проверка живого бота, и который я разрешил сделать после слияния, а не до.
  • Эталоны — эталонные кадры, с которыми сравнивают тесты отрисовки в Ritmolux. Принять эталон — признать новую картинку правильной.

День в единицах, которые что-то значат

Считать коммиты бессмысленно, поэтому сначала то, что коммитами не является:

ПоказательRitmoluxБот расходовВсего
Планов влито в main51116
Выпущено версий31114
Выкладок в прод—88
Запущено headless-сессий385694
Стоимость сессий (условная)$76$135$211
Строк добавлено / удалено (без слияний)+12k/−4k+26k/−2k+38k/−6k

Бот расходов за один день прошёл путь от v0.14.0 до v0.24.0, и каждая из этих версий доехала до прода через CI. Тестов утром было 1052, вечером 1675. Ritmolux, мой музыкальный визуализатор, выпустил три версии и перенёс все эталоны с WARP, программного рендера Windows, на lavapipe — программный Vulkan из Mesa под Linux.

Вот что получили пользователи бота за этот день, по порядку: донаты через Telegram Stars; экспорт в CSV и XLSX; вход по пригласительной ссылке вместо зашитого списка, со страницей о приватности и /delete_account; повторяющиеся расходы и напоминания; импорт банковской выписки в PDF; долги и деление счёта; теги с отчётом по каждому; полное меню команд; онбординг с подсказками; свёрнутые списки с позициями чеков по категориям; и итоги месяца, которые приходят первого числа в 09:00.

Одиннадцать функций, и каждая — отдельный план со своими фазами, тестами и рецензией.

Доллары — это то, что CLI сообщает по каждой сессии. Я работаю по подписке, так что суммы условные: как относительная мера годятся, но это не счёт. Да и упиралось всё не в деньги.

Что это были за коммиты

Я сгруппировал коммиты обоих проектов по префиксу:

ВидКоммитов
docs(plans)79
feat51
fix22
слияния17
всё остальное (docs, test, chore, ci, style)около 35

Самая большая группа — учёт. Семьдесят девять коммитов, больше трети за день, не трогают ничего, кроме файла плана: строка в журнале реализации, блок закрытия, пометка, что фаза отложена, статус, сменившийся на done.

Похоже на накрутку, но дирижёру это нужно. Он верит только тому, что закоммичено. Когда сессия говорит «фазу 3 закончила в коммите a1b2c3», дирижёр проверяет, что коммит существует, лежит на ветке и что строка журнала в плане ссылается именно на него. Поэтому каждое заявление должно стать коммитом, а потом его кто-то читает: дирижёр, когда возобновляет план, рецензент, сессия закрытия. Сделай я ту же работу руками в одной интерактивной сессии, коммитов было бы штук шестьдесят, а понять потом, что происходило, было бы куда труднее.

Ещё семнадцать — слияния: Merge branch 'main' into plan-…. Дорожек по две на проект, main весь день двигался, и каждой дорожке приходилось сначала забирать к себе чужую работу — иначе её прогон тестов ничего не говорил бы о коде, который в итоге окажется в main. Часть слияний сделала сессия слияния дирижёра, часть — я.

Так что настоящая единица — план. Каждый из шестнадцати был написан заранее и одобрен мной, до первой строки кода проверен на противоречия сессией, которая только читает, реализован по фазам, прогнан через полный набор тестов, прорецензирован отдельным процессом, закрыт с новой версией и влит. Коммиты — то, что после этого цикла остаётся.

Всё упёрлось в лимит подписки

В 10:40 пятичасовой лимит аккаунта был выбран на 100 %.

Оба дирижёра работают через один аккаунт. Друг о друге они не знают, и знать им не нужно. CLI просто отказывает в следующем запросе, дирижёр видит событие о лимите, печатает limit reached; waiting 141 min и засыпает до 13:02 по моему времени. В Ritmolux одна маленькая сессия починки показывает 2 часа 12 минут на 14 ходов и $0.42 — почти всё это ожидание. В боте план импорта выписок был активен 3 часа 11 минут, и 2 часа 21 минута из них — то же самое ожидание.

На почасовом графике коммитов это провал: 46 коммитов с 08:00 до 10:00, 15 с 10:00 до 11:00, а потом ни одного до 13:00.

В 16:34 лимит снова был выбран на 98 %. На этот раз его никто не ждал: дорожки Ritmolux как раз ждали меня.

Так что 6 октября скорость дважды упиралась в пятичасовой лимит, и это был самый жёсткий потолок за день. Не модель, не ноутбук, не тесты. Суммы в долларах об этом ничего не говорили: на условные $211 можно было бы купить и больше, но подписка меряет не долларами.

Недельный лимит дорисовывает картину. Он сбросился в 08:00 того же утра, а в 21:01 был выбран на 21 %. Один такой день — примерно пятая часть недели. Значит, так работать каждый день не выйдет, да мне бы и не хотелось.

В Ritmolux был и второй потолок, пониже, — блокировка тестов. Полные прогоны GPU-тестов идут через неё по одному, потому что два прогона одновременно — ровно та нагрузка, при которой раньше падали тесты, чувствительные ко времени. С 08:08 до 10:24 обе дорожки Ritmolux одновременно шли по реализации и стояли в очереди к блокировке, иногда по девять минут. Выручил кэш зелёных прогонов (о нём был прошлый пост): дирижёр помнит, какое именно дерево кода уже проходило тесты. Из 33 запрошенных прогонов 12 пропущены, потому что ровно это дерево уже проходило, а 5 взяли результат прогона, который только что сделала другая дорожка. По-настоящему запускались только 16.

Второй проект получил дирижёра на второй день

Репозиторию бота расходов 6 октября было семь дней. Первый коммит — вечер 29 сентября. К концу 6 октября в нём было 412 коммитов, 34 завершённых плана и 41 ADR, и 120 из этих коммитов — за тот самый день.

Дело не в том, что бот простой. На второй день жизни бота я скопировал в него дирижёра из Ritmolux — в порядке эксперимента. Копия заняла один коммит примерно на 5300 строк, а в её ADR записано, что пришло вместе с ней: «147 коммитов и около 20 последующих ADR с доработками». Все сбои из поста о дирижёре уже случились, были записаны и исправлены в другом репозитории. Pid-файл, который писался раньше, чем появлялся его каталог. Сессия, которая отчиталась об исправлении в коммите, не трогавшем нужный файл. CLI, который запрещает правки в .claude/. Рецензия, которая после парковки начиналась с нуля.

Не менее показательно то, что я при копировании выкинул: блокировку тестов, кэш зелёных прогонов, проверку аннотированного тега. Весь набор тестов бота проходит за 15–40 секунд, блокировать там нечего. В Ritmolux один полный прогон держит блокировку 8–12 минут. Что важно в одном проекте, в другом лишний груз, и при копировании это наконец можно было учесть.

По-моему, именно это упускают, когда говорят о скорости агентов. Со стороны кажется, что день сделали агенты, которые быстро печатают. А на деле большую часть сделали две недели, за которые процесс в Ritmolux отладился до скуки, и потом этот скучный процесс просто скопировали во второй проект. Медленного периода у бота не было вообще.

Что поймали рецензенты

Быстро, но неправильно — бессмысленно, поэтому я прошёлся по журналу рецензий за день.

В Ritmolux ни один раунд рецензии не нашёл блокера. Единственное серьёзное замечание за день было в плане, который переносил эталоны на Linux: «The golden roster asserts in no CI job». Тест, проверяющий, что у каждого эталона есть файл, существовал, но его никто не запускал. Один раунд исправлений, и во втором остались только два мелких замечания.

В боте трём планам понадобился раунд исправлений, остальным — ни одного. И в замечаниях из этих раундов есть закономерность, которая интереснее любой отдельной ошибки.

Одна и та же ошибка снова и снова

Каждая новая функция бота добавляет таблицу. В каждой новой таблице лежат чьи-то данные. А /delete_account должен удалять все данные пользователя, в том числе из таблиц, которых ещё не было, когда /delete_account писали.

  • План с приглашениями добавил записи о донатах. Рецензия, серьёзное: донаты переживают /delete_account. Исправлено.
  • План с повторяющимися расходами добавил правила. Рецензия, блокер: при включённых внешних ключах /delete_account просто падал у любого пользователя, у которого было повторяющееся правило. Исправлено.
  • План с долгами добавил людей, займы и возвраты. Рецензия, серьёзное: таблицы долгов вообще не удалялись. Исправлено.
  • План с тегами добавил «липкий» тег поездки. Рецензия, мелкое, до сих пор открыто: тег остаётся после удаления.

Четыре плана, четыре сессии реализации, все толковые, и каждая забыла одно и то же правило, которое касается всего бота. И каждый раз его ловил свежий рецензент. Рецензия свою работу сделала, но её использовали как память, а она не для этого.

То же самое вышло с зашифрованными книгами расходов. Это необязательный режим сквозного шифрования в боте, когда даже сервер не может прочитать ваши траты. Каждой функции пришлось решать, как она ведёт себя, когда книга зашифрована. Повторяющееся правило копирует шаблон в новый расход, и в зашифрованной книге такую копию было не расшифровать — это поймала проверка готовности ещё до кода. Включение шифрования не шифровало уже существующие долги — серьёзное замечание рецензии. Импорт выписки работает только пока книга открыта. Итоги месяца для закрытой книги приходят вообще без цифр. Каждое из этих решений правильное. Но ни одно не знало о других, каждое принималось в своей сессии, а некоторые — только после вопроса рецензента.

В обычном темпе такое правило держат в голове и месяцами забывают то тут, то там. Здесь его забыли четыре раза за полдня, и всё это видно в журнале рецензий. Если рецензенту за день трижды пришлось напомнить одно и то же правило, этому правилу место в тесте: перебрать все таблицы со столбцом пользователя и проверить, что /delete_account их очищает. Такой тест сделал бы большинство этих замечаний невозможными.

Чем я на самом деле занимался весь день

Ни в одном из 207 коммитов нет трейлера Co-authored-by, автор и коммиттер везде совпадают, так что git не скажет, какие коммиты мои. Зато знают дирижёры. Каждая сессия заканчивается блоком со списком своих коммитов, и дирижёр сверяет его с git. По этим записям примерно две трети дня сделали headless-сессии, за которыми никто не смотрел:

РепозиторийСессии дирижёраСам дирижёрСессии, которые вёл я
Ritmolux59628
Бот расходовоколо 80—около 40

Коммиты «самого дирижёра» — слияния и строки об отложенных фазах, которые программа пишет без всякой модели. Цифры по боту приблизительные, восстановлены по веткам дорожек каждого плана. А локальные репозитории считают по местному времени, поэтому у них выходит 213, а не 207, как у GitHub.

Моя треть меня удивила: кода в ней почти нет. Если пройтись по моим коммитам в Ritmolux по одному: утром два новых 3D-пресета, Ridgeline и Tidepool; правки навыка preset-author; четыре раза руками влил main в дорожку; новая фаза с туманом для плана, который устарел после чужого изменения камеры; написал и одобрил план вместе с его ADR; на глаз оценил три пресета роя (два оставил, третий перенастроил на zoom 0.85, чтобы шов не попадал в кадр); починил падение cargo doc, из-за которого запарковался план; просмотрел 53 перерисованных эталона; дважды принял новые эталоны; написал и поставил в очередь следующий план по уборке. В боте два плана я провёл в интерактивных сессиях, а не через дирижёра: меню команд и онбординг. Всё остальное там того же рода: слияния, правки планов, ADR, одобрения, очередь.

Так что кода я в тот день почти не писал. Одобрял планы, смотрел на картинки и разбирал парковки.

Где машины ждали меня

Лучше всего это видно по парковкам. Ritmolux запарковался одиннадцать раз:

  • Трижды claude_dir: фазе нужно править файл навыка в .claude/, а CLI запрещает это любой headless-сессии, что бы ни было в списке разрешений. В 09:22 я переделал эти правки в отложенные фазы, чтобы сделать их руками после слияния.
  • Трижды plan_wrong. Один раз после слияния, которое принесло изменение общей камеры, не предусмотренное планом, — я добавил в план фазу. Дважды — когда тесты эталонов покраснели: изменение роя с одной дорожки встретилось со свежеперенесёнными эталонами другой, а сессии починки менять эталоны запрещено. Оба раза я принял новые эталоны.
  • Дважды human_phase: оценка пресетов и просмотр тех 53 эталонов.
  • Один раз gate_red: cargo doc, всё ещё красный после одной сессии починки.
  • Один раз disagreement: сессия сделала коммит и не заявила его. Я возобновил её через четыре минуты.
  • Один раз — я сам: последний план дня, после того как упала его сессия реализации.

Бот запарковался четыре раза. Дважды из-за лимита, который я поставил слишком низким: на сессию реализации было $15, два плана упёрлись в него посреди фазы, и я поднял его до $50. Один раз потому, что в плане не был указан файл, который придётся менять; это поймала проверка готовности ещё до трат на реализацию. И один раз из-за номера версии: накануне два плана одновременно заняли v0.13.0.

С 16:45 до 18:04 обе дорожки Ritmolux стояли на парковке и ждали меня, а я отходил по личным делам. Этот час двадцать — то, как скорость выглядит с другой стороны: дирижёр работает не быстрее самого медленного моего решения.

Костыль на 23 минуты

Одна история из второй половины дня хорошо показывает, как такая скорость меняет то, как я принимаю решения.

Эталоны Ritmolux были отрисованы WARP под Windows. Каждый 3D-план на той неделе менял какие-то из них, а задание CI, которое должно ловить такие изменения, было красным ещё с одного из прошлых релизов. План переезда эталонов на lavapipe под Linux уже стоял в очереди, но его ADR считал, что шаг с оценкой — просмотреть 44 перерисованных эталона и решить, какие изменения настоящие, — займёт «дни внимания владельца».

Поэтому в 15:17 я набросал костыль: маленький план для задания CI, которое я мог запустить руками, чтобы принять эталоны WARP по именам, и слияния не стояли до переезда. В 15:30 я его одобрил. Дирижёр влил его в 16:22, за 53 минуты и $7.97.

В 16:45, через двадцать три минуты, фаза 2 другого плана сделала его ненужным: тот план уже стартовал и шёл гораздо быстрее, чем ожидал его собственный ADR. Вечером я просмотрел 53 эталона, а не 44, и все 53 оказались мелкими расхождениями рендера, а не настоящими изменениями. Ушёл на это один вечер, а не дни.

Обычно костыль, который проживёт 23 минуты, — глупая ошибка. Здесь он стоил $8 и 53 минуты машины, которая всё равно ничем другим не была занята, и с тем, что я знал в 15:17, решение было правильным. Ошибиться в том, сколько что-то займёт, стало очень дёшево, а от этого меняется, каких ошибок вообще стоит избегать. Я меньше оценивал и больше решал.

Чем заплачено за скорость

Не всё, что влилось 6 октября, в тот же день и было закончено. Часть скорости появилась за счёт того, что кое-что я сознательно отложил.

Отложенные фазы. В конце дня дайджест Ritmolux показывал: «1 park, 9 owed phases, 11 merges with open findings». В боте фаза «развернуть и открыть» у плана с приглашениями до сих пор отложена. Это мой сознательный выбор, и я сделал бы его снова. План, который ждёт моей пятиминутной проверки на живом боте, держит worktree и блокирует все зависящие от него планы — столько, сколько я до неё добираюсь. Но это долг, и дайджест его честно записывает. Девять отложенных фаз означают, что часть сделанного за день — на самом деле обещание, что я займусь этим позже на неделе.

Красный main. Собственный CI Ritmolux на main в тот день был красным — из-за проверки ссылок. Закрытия всё равно шли по более раннему решению: закрытие не ждёт посторонней красной проверки. Каждое закрытие так и писало: «closing anyway (ADR-0251)». Решение верное, сломанная проверка ссылок не должна останавливать пять планов. Но значит это, что 6 октября дирижёр вливал в ветку, у которой значок CI не был зелёным.

Прод. Примерно через два часа после пуша с планом приглашений я открыл бота, чтобы самому попробовать новое, а он не отвечал. Он падал и перезапускался по кругу. Причина была в .env на сервере: там остался ALLOWED_TELEGRAM_IDS — та самая переменная, которую план только что заменил пригласительными ссылками. Я её убрал, пересоздал контейнер, и бот ожил. Ничто в конвейере не могло этого поймать. Прогон тестов, рецензия и закрытие работают с репозиторием, а сломался файл на сервере, которого в репозитории нет. Восемь выкладок за день — это восемь шансов, что сервер окажется не таким, каким его считает репозиторий, а сервер дирижёр не видит. Вижу его я, но только если пойду и посмотрю. В тот день я нашёл поломку потому, что сам пробовал бота, а не потому, что мне что-то об этом сообщило.

План, который упал. Последний план Ritmolux за день открыл дорожку в 21:00, прошёл проверку готовности за $0.56, а через две минуты его сессия реализации упала. В 21:06 я его запарковал, а на следующее утро доделал руками.

Так какая скорость возможна

Для этого дня и этих двух проектов: шестнадцать прорецензированных, протестированных планов с версиями примерно за четырнадцать часов, от одного человека. И работа не игрушечная: разбор PDF-выписки из банка, долги и деление счёта в разных валютах, 3D-камера, общая для трёх семейств генераторов, переезд всех эталонов на другой рендер. Каждый план прошёл через рецензента, который не видел, как его реализовывали.

Скорость появилась не 6 октября. Её сделали две недели до этого, когда каждый ручной шаг, в котором нечего было решать, превратился в шаг, который выполняет дирижёр, а каждое заявление сессии — в то, что может проверить git.

Каково это было? К концу дня я здорово устал. Но был доволен — и объёмом работы, и её качеством, и второе для меня важнее. И мне кажется, такая скорость — примерно предел для человека, который хочет оставаться в петле. Парковки, 53 эталона, два новых плана вечером, сервер, на который пришлось пойти и посмотреть самому, — всё это я, и места для чего-то ещё почти не оставалось. С большим числом дорожек или лимитом побольше машины пошли бы быстрее, но я перестал бы по-настоящему читать, что они делают. Тогда я бы просто одобрял, а не решал.

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

Поэтому следующий план для бота — не новые функции, а производительность: дочерние процессы, worker threads и вынос тяжёлых задач из основного цикла бота.