Перейти к основному содержимому

Проверка того, что три находки умерли

Сбор урожая первого 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 и rcorigin = 'adopted'; план диагностирует недостающую ветку и ветку вне шаблона; plan_decision остаётся пустым; ни одной строки, вставленной вручную
1bрешить «принять как есть»шаблон не навязывается репозиторию пользователя (RN-045)
2PO создаёт полную историю с 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.

этапсобытиеidseq
1. принятиеbootstrap.repository_adopted01KZCW6SBGZZ5J2DTNKR35EQPC1
1. принятие (как есть)bootstrap.adopted_as_is01KZCW6SETRNVJEX5V018GY5Q02
2. PO предлагаетbacklog.story_promotion_proposed01KZCW6SGJJQX623FW4TQ28ZV33
3. вы продвигаетеbacklog.story_transitioned01KZCW6SH7F14Y1QCJQHNJNF504
4. dev захватываетdev.working01KZCW6SWFY33TF8DDX9MYHG405
4. PR открыт, ждёт gatedev.awaiting_gate01KZCW6TB712EMZCC29WDK7YJC14
4. dev захватываетdev.working01KZCW6VAYQD1R0XH6J3VKKKJ918
4. PR открыт, ждёт gatedev.awaiting_gate01KZCW6VQ3TZSH72CHAECBHZSX27
4. dev захватываетdev.working01KZCW6XA40AVK9N71PPRWKG4N31
4. PR открыт, ждёт gatedev.awaiting_gate01KZCW6XKXYTNW6ARDWF4VRW4240
5. очередь пуста, агент простаиваетdev.idle01KZCW6Y812HAPVG3ZYS2CM6WA43

Три пары dev.working → dev.awaiting_gate — это три tasks, с одним-единственным агентом и без перезапуска engine между ними — находка #10, доказанная исполнением, а не модульным тестом. Финальный dev.idle — это шаг 5: пустая очередь с живым, а не мёртвым процессом.

Чего стоил первый запуск​

Чтобы таблица выше появилась, понадобилось четыре исправления, и стоит зафиксировать, какие, потому что три из них относились к ИНСТРУМЕНТУ, а одно — к ПРОДУКТУ; различие и есть суть.

В инструменте (скрипт и NoopDevAgentServer):

  1. Подопытный репозиторий рождался в os.tmpdir(), локальном для контейнера. Принимает api; клонирует для сборки worktree dev agent, работающий в engine. Bare лежал в /tmp api, и engine его не видел. Он стал рождаться в GIT_LOCAL_REPOS_ROOT, общем томе, — который заголовок самого скрипта уже объявлял предпосылкой.
  2. Noop помечал task in_review и на этом останавливался, не вызывая open_gate: tasks.gate_status оставался NULL. awaiting_gate без открытого gate, и судить нечего.
  3. {:pr_settled, %{opened: true}}, приходящий, когда агент уже в :awaiting_gate, не имел клаузы в Noop. FunctionClauseError, а так как сервер — restart: :temporary, процесс умирал окончательно.

Все три — одна история: Noop был приведён в соответствие с реальным агентом в Фазе 12d, но не успевал за тем, что пришло потом. Это ровно тот риск, который этот документ уже называл, — «находка #10 выживала внутри самого измерительного инструмента», — теперь в трёх новых экземплярах.

В продукте, и это находка, которую находит только реальное исполнение:

  1. При пустой очереди 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 на каждую сданную task1, по построению — свойство находки #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: и называет утверждение, которое не прошло.