Реальная валидация — удалённый GitHub и настоящая модель
Валидация Фазы 12 прямым текстом объявляет собственные
границы: LocalGitProvider, NoopDevAgent, вердикт gate, написанный
самим скриптом. Она существует, чтобы доказать цепочку, не завися ни от сети,
ни от суждения модели, и делает это хорошо.
Эта валидация заменяет ровно те три вещи, которые та оставила за бортом, — удалённый GitHub, реальный dev agent, модель через API, — и результат таков, какого и ждут от честного dogfooding: дешёвая половина прошла целиком, а дорогая провалилась так, как не показывал ни один тест.
Скрипт — pnpm --filter api validacao:real -- --repo <owner/repo>.
Что прошло, и это больше, чем кажется
Всё ниже выполнялось против daneiel/test на настоящем GitHub и без единого
цента расходов — это фаза --ate backlog.
| что | результат |
|---|---|
удалённое принятие (настоящий getRepo) | origin: adopted, ветка по умолчанию main |
| план в dry-run | 6 мутаций, 6 диагностик, решение пустое |
| репозиторий нетронут до решения | проверено через API: ноль веток, ноль содержимого |
| повторное принятие сходится | 6 мутаций на 1-м проходе, 3 на 2-м (RN-046) |
единственная story остаётся draft + предложение | да, без автоматического продвижения |
claimNext до продвижения | null — брать нечего |
| продвижение | зарегистрировано с пользователем в качестве актора |
RN-045 перестаёт быть доказанной только против локального bare: удалённый репозиторий проверен после принятия и был буквально пуст.
Находка D произошла на самом деле
Bootstrap остановился на protect_branches с
Upgrade to GitHub Pro or make this repository public — приватный репозиторий на
бесплатном плане. Это ровно тот сценарий, который задокументировала Фаза D, теперь наблюдаемый
вне теста.
И предпосылка RN-078 подтвердилась: при проверке
репозитория сразу после этого dev, main и qa существовали, а файлы были
закоммичены. Это последний шаг, и единственный, сбой которого оставляет пригодный
репозиторий. Скрипт распознаёт сбой и продолжает — это выход, спроектированный именно для
этого.
Что показал платный прогон
Модель openai/gpt-5-mini через OpenRouter, реальный dev agent, одна story с одной
task: «Expor GET /saudacao».
Task была заблокирована с limite de iterações atingido, источник
modelo. Ни одной PR не открыто, а значит, ни один gate так и не вынес суждения.
Записанная диагностика неудобно ясна: (nenhum terminal rodado).
Измерение
Извлечено с помощью pnpm --filter api medir:execucao, никогда не записывается вручную:
| окно | 1m36s |
| сессии · события | 3 · 59 |
| перезапуск engine посередине | нет |
| круги gate | ни одного (PR не было) |
| немые ходы | ни одного |
| вмешательства пользователя | ни одного |
| агент | вызовы | in | out | стоимость | модель |
|---|---|---|---|---|---|
| dev-api | 8 | 5.671 | 205 | < US$ 0,01 | openai/gpt-5-mini |
| anamnese | 1 | 6.779 | 967 | < US$ 0,01 | deepseek-v4-flash |
| psicologo | 1 | 3.212 | 1.981 | < US$ 0,01 | deepseek-v4-flash |
Критерий ноль перезапусков выполнен. А стоимость — наименее интересная часть: 205 выходных токенов за восемь вызовов — это агент, который ничего не написал.
Что продукт обнаружил сам
Psicólogo работал на тяжёлом tier и выдал три гипотезы. Первая и вторая — верный диагноз, без чьей-либо подсказки:
O dev-api não tinha (ou não utilizou) um ambiente terminal funcional […] em toda a sessão não existe nenhum tool.call de execução de comando […] apenas
search_workspace/read_file.
A ferramenta
search_workspaceestá sub-indexada ou mal configurada para este repositório: não encontrou nenhum arquivo de código ou manifesto […] Isso enganou o agente, que ficou tentando entender "onde está o projeto".
Это заслуживает отдельной записи: интроспекция продукта сработала. Psicólogo прочитал event log неудачного прогона и назвал причину с большей точностью, чем любое утверждение скрипта.
Находки, для сортировки 13c
Дисциплина фазы действует здесь, как и везде: новая находка входит как пункт, никогда как исправление.
X. Dev agent сжигает потолок итераций, исследуя пустой репозиторий
Получив task в только что подготовленном репозитории — где есть только шаблон
Gitflow, без кода, — агент потратил все восемь итераций на
search_workspace/read_file, ища «где проект», и так и не выполнил
ни одной команды и не записал ни одного файла.
Симптом — limite de iterações atingido с источником modelo, что
технически верно и практически бесполезно: модель не ошиблась в суждении,
она так и не дошла до суждения. Причина — отсутствие сигнала о том, что искать
нечего: пустой репозиторий неотличим доступными инструментами от
репозитория, где поиск не удался.
Стоит отметить, что это первый сценарий продукта, в котором dev agent начинает с абсолютного нуля. Каждый тест и каждое демо исходили из workspace с кодом.
Y. search_workspace не отличает «пусто» от «не нашёл»
Прямое следствие предыдущего и, вероятно, та деталь, с которой можно работать: первые пять
вызовов вернули nenhum resultado, что агент прочитал как
«ищи лучше» вместо «здесь ничего нет». Psicólogo пришёл к тому же
выводу самостоятельно.
Второй прогон: исправление Y НЕ закрыло X
После закрытия находки Y тот же прогон выполнили снова — та же story, тот же репозиторий, та же модель. Между ними изменилась только фраза инструмента.
| 1-й прогон | 2-й прогон | |
|---|---|---|
| вызовы dev-api | 8 | 8 |
| выходные токены | 205 | 248 |
| исход | limite de iterações atingido | limite de iterações atingido |
| PR | нет | нет |
Поведение изменилось наблюдаемым образом — один поиск вместо пяти,
за которым следует read_file, — а исход нет. Новое сообщение дошло до агента и
было верным для этого случая (o workspace tem 2 arquivo(s), então a busca funcionou), потому что в репозитории был шаблон Gitflow и он не был
пуст.
Записанная здесь гипотеза была неверной. Прежний текст говорил «a correção é a frase, não o teto». Доказательства говорят другое: из восьми итераций семь ушли на исследование. Остаётся ОДНА на то, чтобы записать файл, закоммитить, сделать push и открыть PR, — с этим не справился бы даже идеальный агент.
TOOL_LOOP_MAX_ITERATIONS по умолчанию равен 8
(apps/engine/config/runtime.exs:100). Это число появилось для разговорного
агента и никогда не пересматривалось для dev agent, которому нужно исследовать
репозиторий, прежде чем действовать.
Psicólogo снова оказался точнее, чем утверждение скрипта:
o workspace não contém o código do projeto: não há package.json, nem src, nem README
ficou apenas em modo leitura/exploração
Чему это учит об измерении
Суть в том, чтобы зафиксировать отрицательный результат. Если бы второй прогон не был сделан, находку Y — реальную и покрытую тестом — было бы легко спутать с решением X. Это было правильное исправление, которое не дало ожидаемого эффекта, и показал это только прогон.
Третий прогон: потолок БЫЛ причиной
С TOOL_LOOP_MAX_ITERATIONS=25 вместо 8 и без каких-либо других изменений dev
agent перешёл на другой уровень:
| инструмент | 2-й прогон (потолок 8) | 3-й прогон (потолок 25) |
|---|---|---|
search_workspace | 2 | 4 |
read_file | 5 | 6 |
write_file | 0 | 3 |
terminal | 0 | 1 |
| исход | лимит итераций | dev.awaiting_approval |
Он исследовал, записал три файла и вызвал npm test --silent. Гипотеза,
записанная выше, была верна: потолок 8 — унаследованный от разговорного
агента — не подходит dev agent, которому нужно понять репозиторий,
прежде чем действовать.
И причина остановки изменилась полностью. Блокировки не было: действие
terminal стало proposed_action с require_approval, и агент перешёл в
dev.awaiting_approval — приостановлен, удерживая worktree и историю, как
спроектировал ADR 0052. Он
остановился и ждал, вместо того чтобы сжигать итерации, стуча в дверь.
Четвёртый прогон и что он показал о Фазе F
Разрешение npm, pnpm, node и npx в allow проекта не разблокировало: агент
выполнил ls -la — глагол, которого не было в списке, — и снова оказался в
ожидании.
Это обнажает разницу между тем, что поставила Фаза F, и тем, что было запрошено.
Запрос был «permita sempre comandos desde que seja na pasta do projeto».
ADR 0055 поставил
потолок: команда, затрагивающая путь вне папки, никогда не одобряется автоматически, сколько бы
глагол ни был в allow. Потолок защищает путь — глагол
по-прежнему управляется allowlist, который по замыслу является закрытым списком.
Практическое следствие: каждая новая команда, которую придумывает агент, попадает в
require_approval. Лестница по-прежнему существует, она лишь стала безопаснее.
Я не считаю это дефектом, а считаю вопросом объёма: ADR 0055 никогда не обещал продвигать глагол. Но разрыв между запросом и поставкой реален и становится пунктом сортировки.
Ловушка самого инструмента
Первая попытка настроить политику не дала никакого эффекта, и причина
заслуживает записи, потому что повторяется: скрипт запускался с хоста, поэтому
PROJECT_WORKSPACES_ROOT упал на значение по умолчанию /tmp/brabo-project-workspaces, и
permissions.json появился в файловой системе, которую engine не видит.
Это та же ловушка подопытного репозитория в /tmp, которую
валидация Фазы 12 уже документирует, — вернувшаяся
другим путём, в тот же день. Заголовок скрипта теперь прямым текстом требует
запуска изнутри контейнера.
Пятый прогон: цепочка доходит до GitHub
При запуске изнутри контейнера (недостававшее условие) и с разрешёнными глаголами terminal цепочка прошла целиком:
| действие | политика | исход |
|---|---|---|
terminal ×2 | auto_approve | ✅ executed |
git_commit | auto_approve | ✅ executed |
git_push | auto_approve | ✅ executed |
pr_open | auto_approve | ❌ failed |
Ветка агента существует на GitHub: feature/task-d4b36a5b, рядом с
dev, main и qa. Код, написанный моделью, закоммиченный под идентичностью
dev-api[bot] и отправленный в настоящий удалённый репозиторий.
pr_open упал с Requires authentication, и причина — находка AA:
api определяет git-токен по action.decidedBy, который равен NULL, когда
политика одобряет автоматически. Push сработал, потому что отправляет engine, который
подставляет учётные данные owner (RN-076).
Шестой прогон: PR открылась на GitHub
С RN-082 на месте цепочка замкнулась до PR:
PR #1 — "Rota pública de saudação — Expor GET /saudacao", из
feature/task-636ef1aa, открыта вdaneiel/test.
Код, написанный моделью, закоммиченный как dev-api[bot], отправленный и
опубликованный как pull request в настоящем удалённом репозитории. Gate открылся
(pr.gate_changed, gateStatus: awaiting_qa), и область QA отработала.
qa-performance-seguranca был корректно освобождён
(delegation.dispensed, обоснование «story sem RNF») — освобождение с
обоснованием, никогда не молчание, как спроектировал ADR 0038.
qa-automacao упал, и это стало находкой AB: он вызвал составную
команду, последний сегмент которой (head) не был в allow, ToolLoop
приостановился в awaiting_approval, а QA Lead классифицировал приостановку как
«desfecho inesperado» с источником infra. Это дефект, который ADR 0052 исправил
для dev agent и который не дошёл до агентов gate.
Седьмой прогон: расширения allowlist не хватило, и это было предсказуемо
С 25 разрешёнными глаголами — явный критерий: то, что ЧИТАЕТ или СОБИРАЕТ, никогда то, что ходит в сеть или разрушает, — gate снова застрял. И не из-за нехватки глагола:
ls -la && echo "---" && cat package.json 2>/dev/null; …
ls, echo и cat все были в allow. Блокирует 2>/dev/null, и
это становится находкой AC: парсер трактует > как разделитель, перенаправление
становится сегментом с глаголом /dev/null, а тот же токен к тому же является абсолютным
путём вне проекта.
Прогноз, записанный до прогона, подтвердился. Allowlist — закрытый список, а модель придумывает команды; расширять список — заплатка. 7-й прогон доказывает это так, что трудно оспорить: 25 глаголов, а застрял на ФОРМЕ команды, а не на глаголе.
Восьмой прогон и аргумент, который он закрывает
С исправленными Y, AA, AB и AC dev agent сделал единственный вызов:
bash -lc npm test --silent. Глагол bash, вне списка, require_approval.
Отказ правилен — разрешение bash обнулило бы весь allowlist, включая
встроенные deny. Но три последних прогона вместе говорят то, чего
ни один из них не говорил по отдельности:
| прогон | застрял на | категория |
|---|---|---|
| 6-й | head | глагол |
| 7-й | 2>/dev/null | форма |
| 8-й | bash -lc | вызов |
Три разные категории за три раунда. Расширение списка решает первую и не затрагивает две другие. Allowlist глаголов не сходится против агента, который свободно выбирает, как вызвать то, что хочет запустить.
Это не дефект allowlist: он выполняет то, что обещает, и отказ в bash
— доказательство, что граница держит. Это граница ОБЪЁМА — он не был
спроектирован для обеспечения автономии и не обеспечивает её.
Практический вывод 13b таков, и он ценнее PR: путь к gate через LLM не проходит через ослабление политики. Он проходит через то, чтобы агент ждал решения, а не умирал (находка AB), — что ADR 0052 уже сделал для dev agent.
Девятый прогон: дешёвое исправление, которое уничтожило бы гарантию
Девятый застрял на том же bash, что и восьмой, и очевидное исправление было в одной строке:
положить bash в allow и смотреть, как конвейер становится зелёным.
Этого не сделали, и в этом результат прогона. Разрешение bash не расширяет
allowlist — оно его обнуляет, потому что у каждой заблокированной команды появляется разрешённая
форма вызова, включая встроенные deny. Раунд прошёл бы,
а гарантия исчезла бы, и ничто в результате не указало бы на подмену.
Поэтому девятый закрепил диагноз: проблема никогда не была в том, какой глагол есть в списке, а в том, что агент gate умирал, когда политика велела спросить. Отсюда появился ADR 0057, распространяющий на gate то, что ADR 0052 уже сделал для dev agent: перед действием, требующим решения, приостановиться и ждать, а не классифицировать собственную приостановку как сбой infra (находка AB).
Десятый прогон: вся цепочка, от начала до конца
С ADR 0057 на месте десятый закрыл всё, что девять предыдущих оставили открытым, без единого перезапуска engine:
| этап | исход |
|---|---|
| удалённое принятие | origin: adopted против daneiel/test |
| план репозитория | выполнен только после вашего решения |
| продвижение story | вручную, с пользователем в качестве актора |
| реальный dev agent | написал код, закоммитил как <agente>[bot] |
| push и удалённая PR | опубликована на GitHub |
| gate | открылся (pr.gate_changed) |
| область QA | делегировала и освободила с обоснованием |
| субагент | приостановился на одобрении и не умер |
| ваш отказ | возобновил цикл вместо того, чтобы его завершить |
| вердикт | changes_requested, вынесен LLM |
Важны две последние строки. То, что субагент остановился и остался жив, — это работающий ADR 0057; отказ пользователя, возобновляющий цикл, — это точка, до которой не доходил ни один предыдущий прогон: человеческое решение входит посреди работы агента, и он продолжает оттуда, а не начинает заново и не сдаётся.
И вердикт не был написан скриптом: он вышел из суждения модели о реальной PR. Это ровно та разница, ради которой существует эта валидация по сравнению с детерминированной сестрой.
TODO(humano): стоимость в долларах и число вызовов этих двух прогонов в своё время не были извлечены с помощью
medir:execucao. Еслиtoken_usageсессий ещё сохранился, стоит заполнить — остальные измерения этого документа все получены скриптом, и эти два — исключение.
Прогоны с двумя модулями: параллелизм на проверке
Первые десять выполнялись с одним модулем. Этого достаточно, чтобы доказать цепочку, и недостаточно, чтобы доказать параллелизм: с единственной историей Dev Lead отказывается параллелить — «столкнулись бы в одних и тех же файлах» — и он прав. Потолок RN-083 никогда не запрашивался реальной работой.
Затем пришли три раунда с --modulos 2 (одна история в api, одна в
web). Первые два описаны в
находке AF — тот, что сломался, и тот, что доказал
исправление. Далее — третий, существующий для измерения.
Dev Lead спланировал сам:
2 agentes em 2 módulos — "cada módulo tem exatamente uma história, então um agente por módulo é o mínimo justificável sem desperdício."
И раунд закрылся, измеренный через medir:execucao, а не вручную:
| длительность | 3m56s, 182 события в 3 сессиях |
| вызовы | 33 (dev-api 10, dev-web 9, arquiteto 7, qa-automacao 6, dev-lead 1) |
| стоимость | < US$ 0,01 |
| перезапуск engine | нет |
| немые ходы | ни одного |
| gates | qa approved, secops approved |
Потолок потребовал решения. С двумя работающими агентами следующий запрос остановился
в aguardando_autorizacao с действием parallelize, ожидающим в базе — 2
активных, потолок 2. Ничего не поднялось: если бы поднялось, авторизация была бы театром.
Что сломали два модуля до этого раунда
Этот раунд прошёл чисто, но он третий с двумя модулями, и цену заплатили
два первых. В первом dev-web взял task и умер
на fatal: not a git repository до первого хода — ноль потраченных токенов,
task заблокирована. Это находка AF: охрана быстрого
пути Workspace.ensure!/4 спрашивала, существует ли .git, а git init создаёт
.git до fetch; второй агент читал «готово» и пропускал lock.
Lock существовал с Фазы 4 и был корректен. Ошибочным был
критерий, решавший, стоит ли его брать, — и ни один из десяти предыдущих
прогонов не мог это показать, потому что ни в одном не было второго агента.
После исправления dev-web перешёл от 0 к 16 вызовам в следующем раунде, и именно отсюда
этот третий наследует право быть просто измерением.
Инструмент тоже научился: утверждение о потолке проверяло число (3-й запрос просит авторизацию), что было верно для одного модуля и провалило прогон, в котором продукт действовал правильно, — с двумя модулями активация уже заполняет потолок. Теперь оно проверяет правило: пока помещается, поднимается без вопроса; когда не помещается, останавливается.
Чего эта валидация ещё НЕ доказывает
Честно об охвате, как и в её сестре:
- Merge. По-прежнему вне по замыслу (RN-014) и останется так: кнопку в защищённой ветке нажимаете вы.
- Остальные пять providers. Только OpenRouter работал с реальными учётными данными; остальные по-прежнему без smoke из-за отсутствия ключа, а то, что верно для одного provider, не переносится на другие рассуждением.
- Изоляция. Агент выполняется в том же контейнере, что и этот monorepo.
ADR 0055 говорит о себе, что это политика, а не
изоляция —
..проваливается, но symlink изнутри наружу не обнаруживается. - Автономия без политики на пути. Allowlist глаголов не сходится (находки Z и AD), и это граница объёма, а не баг для исправления.
- Потолок параллелизма, запрошенный САМИМ Dev Lead. С двумя модулями он спланировал 2 агента, а потолок 2 — поместилось. Потолок превысил сценарий, вызывая use case напрямую. Чтобы увидеть, как lead просит больше, чем может, понадобились бы 3+ модуля, а это ещё не запускалось.
Сама цепочка — от принятия до вердикта gate через LLM — доказана на реальной сети. То, что остаётся открытым выше, — не цепочка, а среда, в которой она работает, и поверхность, которую она покрывает.
Ссылки
- validacao-fase-12.md — детерминированная сестра
- achados-execucao-real.md — урожай
- backlog.md — куда идут X и Y