Проверка того, что три находки умерли
Сбор урожая первого dogfooding оставил три находки P1 по эксплуатируемости. Фаза 12 устранила их одну за другой; этот документ — доказательство того, что все три умерли в одном и том же запуске, а не только в модульных тестах, которые каждый срез написал для себя.
Проверка — это скрипт: pnpm --filter api validacao:fase-12. Он завершается с
ненулевым кодом, когда критерий не выполняется, — это критерий приёмки, а не
отчёт, — и в конце печатает таблицу доказательств, прочитанную из базы.
Идентификаторы ниже не переписаны вручную.
Чего она НЕ доказывает
Это идёт первым, а не в сноске, потому что проверка, скрывающая собственные пределы, стоит меньше, чем никакая.
Не доказывает удалённый GitHub. Принятие выполняется против
LocalGitProvider. Причина не в удобстве: fork, использованный в Фазе 10,
так и не был назван — строка 135 миссии по-прежнему
TODO(humano): qual owner/repo do fork?, — так что цели для повторного
принятия не существует. Проверяемый путь одинаков для обоих providers
(getRepo → план → origin: 'adopted'); меняется сеть, и эта разница покрыта
smoke adopt-repository.smoke.spec.ts, который запускается только с реальными
ADOPT_TEST_REPO и GITHUB_TEST_TOKEN.
Не доказывает суждение gates. QA и SecOps — агенты LLM. Здесь вердикт
входит через RecordGateVerdictUseCase — это реальная воронка, через
которую проходит их заключение и где рождается строка outbox
task.gate_resolved. Фазе 12b нужно доказать цепочку вердикт → outbox → wake →
claim, а не то, умеет ли модель читать suite. Суждение по-прежнему покрыто
приёмками Фазы 4a, которые ADR 0020
явно объявляет недетерминированными с локальной моделью.
Не делает merge. Merge в защищённую ветку — решение пользователя, по
замыслу (RN-014). Шаг 6 сценария показывает,
как замок отклоняет ровно это, при автономии auto_approve и
разрешающем permissions.json.
Нигде не использует LLM. Dev — это NoopDevAgentServer. Он не пишет
настоящий код — но начиная с Фазы 12d он проходит ту же машину состояний,
что и реальный агент (Engine.Dev.AgentIo), и именно она здесь под тестом. До
этого Noop обрабатывал одну task и останавливался: находка #10 выживала внутри
самого измерительного инструмента, что провалило бы критерий «ноль
перезапусков» из-за дефекта инструмента, а не продукта.
Сценарий
| # | шаг | что утверждается |
|---|---|---|
| 0 | создать проект | рождается с story_promotion = manual без какой-либо настройки |
| 1 | принять заранее существующий bare-репозиторий с main и develop, без qa и rc | origin = 'adopted'; план диагностирует недостающую ветку и ветку вне шаблона; plan_decision остаётся пустым; ни одной строки, вставленной вручную |
| 1b | решить «принять как есть» | шаблон не навязывается репозиторию пользователя (RN-045) |
| 2 | PO создаёт полную историю с 3 задачами | история становится draft + proposed_ready; claimNext возвращает null |
| 3 | пользователь продвигает | история становится ready; предложение уходит из очереди; событие фиксирует user, а не agent/po |
| 4 | активировать исполнение и разрешить 3 gates последовательно | 3 задачи, 1 агент, 0 перезапусков engine |
| 5 | очередь пуста | явный dev.idle, процесс жив |
| 6 | предложить merge в защищённую ветку при всём разрешённом | pending — это по-прежнему ваше решение |
Шаг 2 — то, что проверяемо убивает находку #13: недостаточно, чтобы история
стала draft, нужно, чтобы ничего не было доступно для захвата. Поэтому
скрипт вызывает claimNext напрямую и требует null. Шаг 4 — то, что убивает
#10: со второго круга никто не запускает :work — агент захватывает сам,
разбуженный task.gate_resolved предыдущего круга.
Доказательства
Вставьте сюда таблицу, которую печатает скрипт. Каждая строка — это
session_events.id (ULID), существующий в базе и доступный для запроса позже.
Выполнено 2026-08-07, выход 0, при поднятом dev-стеке и скрипте,
запущенном изнутри контейнера api. Идентификаторы ниже взяты из базы в том же
запуске — проект f84f7226, сессия backlog 680ab9e9, сессия исполнения
91f384fa.
| этап | событие | id | seq |
|---|---|---|---|
| 1. принятие | bootstrap.repository_adopted | 01KZCW6SBGZZ5J2DTNKR35EQPC | 1 |
| 1. принятие (как есть) | bootstrap.adopted_as_is | 01KZCW6SETRNVJEX5V018GY5Q0 | 2 |
| 2. PO предлагает | backlog.story_promotion_proposed | 01KZCW6SGJJQX623FW4TQ28ZV3 | 3 |
| 3. вы продвигаете | backlog.story_transitioned | 01KZCW6SH7F14Y1QCJQHNJNF50 | 4 |
| 4. dev захватывает | dev.working | 01KZCW6SWFY33TF8DDX9MYHG40 | 5 |
| 4. PR открыт, ждёт gate | dev.awaiting_gate | 01KZCW6TB712EMZCC29WDK7YJC | 14 |
| 4. dev захватывает | dev.working | 01KZCW6VAYQD1R0XH6J3VKKKJ9 | 18 |
| 4. PR открыт, ждёт gate | dev.awaiting_gate | 01KZCW6VQ3TZSH72CHAECBHZSX | 27 |
| 4. dev захватывает | dev.working | 01KZCW6XA40AVK9N71PPRWKG4N | 31 |
| 4. PR открыт, ждёт gate | dev.awaiting_gate | 01KZCW6XKXYTNW6ARDWF4VRW42 | 40 |
| 5. очередь пуста, агент простаивает | dev.idle | 01KZCW6Y812HAPVG3ZYS2CM6WA | 43 |
Три пары dev.working → dev.awaiting_gate — это три tasks, с одним-единственным
агентом и без перезапуска engine между ними — находка #10, доказанная
исполнением, а не модульным тестом. Финальный dev.idle — это шаг 5: пустая
очередь с живым, а не мёртвым процессом.
Чего стоил первый запуск
Чтобы таблица выше появилась, понадобилось четыре исправления, и стоит зафиксировать, какие, потому что три из них относились к ИНСТРУМЕНТУ, а одно — к ПРОДУКТУ; различие и есть суть.
В инструменте (скрипт и NoopDevAgentServer):
- Подопытный репозиторий рождался в
os.tmpdir(), локальном для контейнера. Принимает api; клонирует для сборки worktree dev agent, работающий в engine. Bare лежал в/tmpapi, и engine его не видел. Он стал рождаться вGIT_LOCAL_REPOS_ROOT, общем томе, — который заголовок самого скрипта уже объявлял предпосылкой. - Noop помечал task
in_reviewи на этом останавливался, не вызываяopen_gate:tasks.gate_statusоставался NULL.awaiting_gateбез открытого gate, и судить нечего. {:pr_settled, %{opened: true}}, приходящий, когда агент уже в:awaiting_gate, не имел клаузы в Noop.FunctionClauseError, а так как сервер —restart: :temporary, процесс умирал окончательно.
Все три — одна история: Noop был приведён в соответствие с реальным агентом в Фазе 12d, но не успевал за тем, что пришло потом. Это ровно тот риск, который этот документ уже называл, — «находка #10 выживала внутри самого измерительного инструмента», — теперь в трёх новых экземплярах.
В продукте, и это находка, которую находит только реальное исполнение:
-
При пустой очереди
POST /internal/sessions/:id/tasks/claimотвечает201сcontent-length: 0. Use case возвращаетnull, но NestJS сериализует это как ПУСТОЕ тело;Reqотдаёт"", что не являетсяnil, иAgentIo.try_claim/2сопоставлялся с клаузой найденной task — вызываяrun_task("")и падая сBadMapError.try_claim/2живёт вAgentIo, общем с реальнымDevAgentServer: каждый dev agent умирал в тот момент, когда очередь модуля пустела, — а это самый распространённый исход, какой только есть. И умирал окончательно — серверrestart: :temporary, аMonitorстирал строку состояния следом. Точная противоположность тому, что поставила Фаза 12b: вместо супервизируемого и пробуждаемого событиемdev.idle— мёртвый процесс.Набор тестов этого не ловил, потому что фейк корректно возвращает
nil. Исправлено на границе (claim_task/4) и защищено в контракте (try_claim/2), без изменения HTTP-статуса маршрута.
Находка 4 — эмпирический ответ на вопрос, ради которого существует эта фаза: что доказывает реальное исполнение, чего не доказывает тест.
Скрипт отказывается завершаться успешно, если какой-либо этап, который он заявил как проверенный, не оставил доказательства в event log, — без этой проверки ошибочный запрос дал бы короткую таблицу, и проверка всё равно прошла бы, а это классический режим отказа сгенерированного отчёта.
Было × стало
Колонка Фаза 10 приводит только то, что выводимо из записанного. Всё, что
зависело бы от живого подсчёта, отображается как не измерено, по причине,
объяснённой в сборе урожая.
| Фаза 10 | Сейчас | |
|---|---|---|
| направить проект на существующий репозиторий | ручной seed в две таблицы, до первой сессии (dogfooding-mission.md:104-134) | маршрут принятия; origin = 'adopted'; ноль ручных записей |
| расходящаяся политика репозитория | диагностики не было — bootstrap был единственным путём, и он навязывал шаблон | план в dry-run, который описывает расхождение и ничего не применяет без одобрения |
| перезапуски engine на каждую сданную task | 1, по построению — свойство находки #10 (:666), не оценка. Реальный итог: не измерено | 0 |
| партии | вся фаза шла партиями (:393-416) | их нет: агент проходит очередь модуля сам |
| агент без task | мёртвый процесс (restart: :temporary) | явный idle, супервизируемый, пробуждаемый событием |
| серия отказов | сжигала бюджет подряд | circuit breaker останавливается в idle_tripped (RN-047) |
история → ready | автоматически при создании, без человеческого шага (находка #13, :669) | решение пользователя, с актором, записанным в event log (RN-048) |
| отклонить историю | не было ни состояния, ни события, ни кнопки (находка #14) | возврат PO с причиной, закреплённой в его сессии |
| всего ручных вмешательств | не измерено — таблица наблюдений осталась пустой (:488-490) | те, что в конвейере одобрений, который фаза не меняла |
| merge в защищённую ветку | вручную, по замыслу | вручную, по замыслу — без изменений, и шаг 6 это доказывает |
Последняя строка важна не меньше остальных. Фаза 12 — о том, чтобы агент не умирал между задачами, и о том, чтобы решение вернулось к пользователю; она не расширяет никакую автономию. Конвейер одобрений ровно такой, каким был.
Как запустить
# a stack de dev de pé (api e engine compartilhando /data)
pnpm dev
# de dentro do container da api
docker compose -f docker/docker-compose.yml exec api \
pnpm --filter api validacao:fase-12
Если какой-то критерий не выполняется, скрипт говорит, какой, — сообщение
начинается с CRITÉRIO NÃO FECHOU: и называет утверждение, которое не прошло.