Находки реального прогона конвейера
Собраны в сессии исполнения, проведённой через браузер 2026-08-05, с моделью по API (не локальной) и настоящим git-провайдером. Это входные данные для триажа FASE 13c: у каждого пункта есть файл:строка или событие, которое его доказывает, и ни один не был исправлен «попутно» — у закрытых указан PR.
Проект СОЗДАН (не принят) в GitHub через мастер. Реальный репозиторий:
daneiel/hello-api, приватный. Сессия мастера: f15b0cc9. Здоровая сессия:
36abf7e7.
Исправлено во время прогона (блокировало)
- Измеритель не измерял (PR #136, смержен) —
db.executeдеструктурировался как массив + агент читался из актора события вместо payload. llm_turn_streamбезreceive_timeout(PR #137, смержен) — падал в 15 с по умолчанию у Req; четыре разговорных агента проходят только там.ApprovalCardронял экран сессии (будет закоммичено) —ActionTypeв web был подмножеством backend;ACTION_ICON[actionType]→undefinedдляgit_repo_create/git_branch_create/git_branch_protect, а это именно то, что порождает ЛЮБОЙ проект, созданный у провайдера. «Универсальный запасной вариант» существовал только в комментарии. Не проявлялось лишь в ПРИНЯТОМ проекте (без bootstrap).
Открыто — для триажа 13c
A. Сессия provisioning рождается без процесса в engine (СЕРЬЁЗНО)
ЗАКРЫТО — стало RN-067. Четыре места вызова, перечисленные ниже, стали создавать сессию через
CreateSessionUseCase(provision-repository:119и:125,adopt-repository,activate-execution), и правило объявляет его единственным местом, создающим сессию. Записано, потому что доказательство от противного ниже — то, что сделало дефект видимым.
CreateSessionUseCase — единственное место, порождающее session.created
в outbox. Создают сессию напрямую в репозитории, минуя это:
apps/api/src/application/use-cases/git/provision-repository.use-case.ts:112и:121apps/api/src/application/use-cases/git/adopt-repository.use-case.ts:176apps/api/src/application/use-cases/execution/activate-execution.use-case.ts:156← это сессия, в которой работают DEV AGENTS
Эффект: engine никогда не узнаёт о существовании сессии → вечный
REFUSED JOIN, без канала, без живого обновления, без heartbeat, и сессия
никогда не закрывается (остаётся active навсегда). UI жалуется только в
консоли.
Доказательство от противного:
| сессия | session.created | engine.session_states | канал |
|---|---|---|---|
мастер f15b0cc9 | нет | пусто | REFUSED JOIN |
обычный маршрут 36abf7e7 | да | active | JOINED |
B. Стартовая модель (РЕШЕНИЕ ПОЛЬЗОВАТЕЛЯ)
Сессия всегда рождается на модели по умолчанию workspace (llama3.2:1b,
локальной) — ADR 0020 запрещает локальные 7B/1B на семантическом шаге, и
пришлось вручную менять в обеих сессиях. Запрос: настраиваемая стартовая
модель, наследующая модель Criativo в этом сценарии, потому что он
всегда — входная дверь проекта.
C. Говорит модель, а не агент (ДЕФЕКТ, указанный пользователем)
Пузырь, транслируемый вживую, подписан именем МОДЕЛИ
("DeepSeek V4 Flash Latest"); агент (po) появляется только с приходом
сохранённого события. Эффекты: сначала появляется неверное имя, а
сообщение на экране ДУБЛИРУЕТСЯ (пузырь потока + пузырь события). Поток не
сверяется с сохранённым событием.
D. Невыполнимый шаг блокирует мастер без выхода
Proteger branches падает в приватном репозитории на бесплатном плане — сам
мастер ПРЕДУПРЕЖДАЕТ об этом заранее. Но единственное действие, предлагаемое
потом, — «Tentar novamente», которое будет падать всегда. Не хватает
признать это и продолжить.
E. Превью репозитория лжёт
В apps/web/src/routes/NewProjectWizard.tsx:331 зашито
repo: brabo/{slug}. Настоящий владелец берётся из PAT
(createForAuthenticatedUser), то есть daneiel/hello-api. Ошибка доходит до
экрана ПОДТВЕРЖДЕНИЯ.
F. Мастер анонсирует rc
Шаг «Política de branches» перечисляет rc среди постоянных и rc ← qa в
каскаде. Действующая политика содержит только dev/qa/main; возврат
rc находится в backlog ADR 0030.
G. Приглашение Criativo не появляется в созданном проекте
Пустое состояние «A vez é sua» отрисовывается только при пустой ленте. В созданном проекте в ленте уже есть карточки bootstrap, поэтому пользователь не получает никакого приглашения.
H. Обезличенная лента активности
Все 10 событий bootstrap отображаются как «atividade em system», без указания, что произошло.
I. Карточка действия показывает ТЕКУЩУЮ модель сессии
Смена модели сессии задним числом переписывает подпись на карточках старых
действий. token_usage замораживает правильную цену; дело только в экране.
J. Psicólogo запускается в пустой сессии
ЗАКРЫТО — стало RN-079. Анализ запускается, только если есть АНАЛИЗИРУЕМОЕ событие, а «анализируемое» не учитывает машинные шаги bootstrap и след самих аналитиков — без этого второго вычета первый анализ навсегда сделал бы сессию заполненной. Без материала выдаётся
psychologist.analysis_skipped, и ничего не тратится. Сессия из находки воспроизведена как тест (14 событий, ни одного анализируемого). В сессииb2fceb9e, только что открытой: получил гипотезы предыдущей сессии с логом новой (пустым), попытался сослаться на несуществующиеseq 60-78, дважды получил отказ в доказательстве и сдался (psychologist.analysis_failed). Проверка удержала выдумку — но запускать анализ в сессии без событий — пустая трата.
K. Дублирующееся бизнес-правило не дедуплицируется
ЗАКРЫТО ЧАСТИЧНО — стало RN-080. ТОЧНЫЙ дубликат (тот же заголовок без учёта регистра, диакритики и пробелов) отвергается при выпуске, в рамках проекта — именно между сессиями он и рождается. Семантический дубликат остаётся открытым и так и объявлен: отличить «Saudação com nome» от «Quem chama pode se identificar» — это суждение, а не
if. Двукратный запуск Criativo в одном проекте оставил 10 правил, 5 из них сиротами («descoberta — sem história»). Эффект моего сценария, а не продукта, но он показывает, что нет ни дедупликации, ни предупреждения.
L. Кнопка в подвале устаревает
Продолжает показывать «Estou pronto para produzir» после того, как лента уже перешла к PO.
Сработало так, как задумано (тоже записать)
- RN-059: долговременный
agent.errorсorigem: infra, агент говорит в ленте и возвращается вidle(замечено 2 раза на таймаутах 15 с). - Проверка доказательств Psicólogo, отвергающая выдуманные id событий.
- Bootstrap Gitflow в GitHub: 4/5 шагов, реальный репозиторий с
main/dev/qa. - Конвейер proposed_action: 6 действий bootstrap, автоматически одобренных политикой.
- 12c: ручное продвижение по умолчанию,
story_promotion_proposedкак предложение. - Прослеживаемость правило ↔ история, с разделением покрытых и непокрытых.
- Живой прогресс bootstrap, где сбой показывает сообщение GitHub.
M. ARQUITETO СЛЕП К СОБСТВЕННОМУ module_map (P1 — сбой прогона)
ЗАКРЫТО — стало RN-066 и подтверждено в production в последнем разделе этой страницы: 4 вызова
assign_story_modulesвместо 18, ни одного выдуманного имени, 1 module_map вместо 4.
Сессия 36abf7e7, seq 80-131. Arquiteto выпустил карту (модули saudacao и
api_http) и сразу после этого не смог её перечитать. Нет инструмента
для чтения действующего module_map, а отказ assign_story_modules не
возвращает допустимые имена. Результат: перебор.
18+ попыток наугад: api, core, http, greeting, domain, web,
hello-api, hello, greeting-api, saudacao (угадал по везению), app,
server, publico, public-api, api-publica, …
Его собственными словами (seq 94, 99, 124):
"os nomes que tentei (
api,core) não batem. Vou descobrir os nomes válidos testando candidatos plausíveis" "Preciso descobrir os nomes reais dos 2 módulos. Vou testar candidatos adicionais."
Три последствия, по степени тяжести:
- Неверные данные, объявленные верными. Все 4 истории оказались в
["saudacao"], включая историю ENDPOINT.api_httpостался вообще без истории. А исход (seq 130) утверждает: «Todas as 4 histórias foram vinculadas com sucesso aos módulos.» Лог заканчивается уверенной ложью. - Ломает исполнение ниже по течению.
activate-executionподнимает по одному dev agent на МОДУЛЬ. Когда всё вsaudacao, модульapi_httpне получает агента, и спроектированная архитектура — не та, что будет построена. - Ни один
tool.resultне записывается дляassign_story_modules. Весь цикл невидим в event log — его можно лишь вывести по повторяющимсяtool.call.
Стоимость цикла: 9 вызовов LLM у архитектора, 27.804 вход / 8.012 выход. Итого по сессии: 7.271 микро (US$ 0,007) — дёшево только потому, что модель flash.
Цикл module_map (PR #135) был СИМПТОМОМ этого: Arquiteto перевыпускал карту именно для того, чтобы попытаться закрепить имена, которые не мог прочитать. #135 закрыл порчу данных; слепота осталась.
Исполнение (проект 17229425, сессия исполнения dbb84ce8) — 2026-08-05
N. ПОЛОВИНА ИСПОЛНЕНИЯ РАБОТАЕТ ТОЛЬКО С ПРОВАЙДЕРОМ local (P1, блокирует 13b)
Engine.Projects.ProjectRepository.get_local_repo_path/1 возвращает
{:error, {:unsupported_provider, "github"}} для любого провайдера, кроме
local. Это задокументировано как сокращение рамок в самом moduledoc:
"Só suporta o provider 'local' (github/gitlab remotos ficam fora de escopo do executor de terminal por ora)" —
apps/engine/lib/engine/projects/project_repository.ex:25-27
От него зависят пять мест вызова:
lib/engine/dev/worktree_manager.ex:20— worktree dev agentlib/engine/actions/terminal_executor.ex:39— выполнение командыlib/engine/gates/diff.ex:17— diff, который читают QA и SecOpslib/engine/harness/project_context.ex:30— контекст проекта
Наблюдаемый эффект: я активировал исполнение в проекте GitHub, 3 dev agents
поднялись, взяли по задаче, и все 3 были заблокированы в одну и ту же
секунду с falha ao preparar o worktree.
Ключ — асимметрия: api говорит с GitHub по HTTP (именно она создала репозиторий, закоммитила шаблон и создала dev/qa). Engine работает в файловой системе и знает только локальный bare-репозиторий. Поэтому проект в GitHub выполняет РАЗГОВОРНУЮ половину (Criativo, PO, Arquiteto) и bootstrap, но не половину ПОСТРОЕНИЯ.
Последствие для FASE 13b: сценарий из CLAUDE.md («ПРИНЯТЫЙ проект из форка через удалённый GithubProvider, настоящий DevAgent, dev реализует → удалённый PR → gates») сегодня невыполним. Это не попутный баг: поддержка удалённого требует clone, учётных данных внутри engine и push — фича с ADR.
O. Dev agent рождается на локальной модели (тот же корень, что у находки B)
Три dev agents поднялись с Llama 3.2 1B (local), унаследованной от
значения по умолчанию workspace. ADR 0020 запрещает маленькую локальную
модель на семантическом шаге, а dev agent, пишущий код, — самый дорогой
семантический шаг из существующих.
P. Событие блокировки без источника
{"origin": null, "reason": "falha ao preparar o worktree", "diagnosis": "{:unsupported_provider, \"github\"}"}. CLAUDE.md требует, чтобы любой исход
сбоя записывал ИСТОЧНИК (infra | modelo | código | política). Здесь это
código (известное ограничение продукта), а пришло null. Диагноз спас
положение, но правило не было соблюдено.
Q. agent.error с «неопределённым источником»
В Criativo ход прерван: лента показала falha · origem indeterminada.
RN-059 сработало (долговременная ошибка, агент сказал, чистое
возобновление), но «indeterminada» — не один из четырёх источников, это
диагноз методом исключения, который ADR 0020 запрещает.
R. PO породил перекрывающиеся истории
ЗАКРЫТО ЧАСТИЧНО — стало RN-081. Идентичный заголовок отвергается; история, не добавляющая покрытия правилам, которые она цитирует, становится
backlog.story_overlap_warned, предупреждением, а не блокировкой. Точная пара из этой находки по-прежнему проходит — у разных заголовков и обоснований для одного endpoint нет ничего механического, что их связывало бы. Есть тест, утверждающий это ограничение, чтобы оно было видимым, а не неявным. «Endpoint público de saudação determinística» и «Endpoint público GET /hello que responde saudação imediata» покрывают один и тот же endpoint. Ни дедупликации, ни предупреждения.
Прогон hello-limpo (проект 9c7c84f0, сессия 1f94de49) — 2026-08-06
Настоящий dev agent, DeepSeek V4 Flash через OpenRouter, с включённым конвейером одобрения и каждым действием, решённым вручную. Задача была «Expor rota GET pública /api/saudacao». Она так и не началась: 18 ходов, 292.211 входных токенов, US$ 0,0275 и ноль написанных строк, и прогон закончился ошибкой провайдера.
S. Накопленный контекст превышает лимит провайдера и УБИВАЕТ исполнение (P1)
На 18-м ходе вызов модели вернул
{413, %{"message" => "request entity too large", "statusCode" => 413}},
ToolLoop не смог продолжить, и задача была заблокирована (dev.blocked,
artifact.task_blocked, seq 151–152).
Причина механическая и накопительная: каждая терминальная команда
вываливает весь вывод в историю цикла, а история идёт вместе с каждым
следующим ходом. Самый большой УСПЕШНЫЙ запрос, записанный в token_usage,
был на 28.993 входных токена; упавший не успел записать расход. Превышение
касается размера запроса в байтах, а не окна контекста — find или
git ls-files с длинным выводом весит в байтах гораздо больше, чем в
полезных токенах.
Это напрямую связано с ADR 0055, под углом, которого ADR не предвидел: лестница одобрений не только удорожает исполнение — она его убивает. Каждый вопрос пользователю подталкивает агента ещё к одной исследовательской команде, вывод которой остаётся в истории навсегда. Гонка заканчивается 413 раньше, чем будет написана первая строка кода.
Триажу нужно решить, чьё это исправление — ContextManager (сжимать или
обрезать вывод инструментов по размеру, а не только по давности),
терминального исполнителя (потолок байт на вывод, с меткой обрезки) или
обоих. Сегодня потолка нет нигде на этом пути.
- Доказательство:
session_eventsseq 150–152 сессии1f94de49;token_usageактораdev-http-api.
T. Источник сбоя по-прежнему вне четырёх (повторение P и Q)
dev.blocked из находки S записал:
{
"origem": "indeterminada",
"reason": "parou sem concluir nem reportar bloqueio",
"diagnosis": "falha na chamada ao modelo: {413, %{\"message\" => \"request entity too large\", \"statusCode\" => 413}}"
}
Это не новая находка: это третье нарушение того же правила, и поэтому
оно записано как повторение, а не отдельный пункт. P
поймал origin: null в dev.blocked; Q
поймал "indeterminada" в agent.error. Здесь они встречаются: событие
блокировки, значение "indeterminada".
Что добавляет этот случай и что делает его сильнее предыдущих: источник
был тривиально выводим. Известный HTTP-статус провайдера — это modelo,
без какой-либо двусмысленности, — само поле diagnosis называет его в той
же строке, где origem сдаётся. Это не пограничный случай, а путь ошибки,
не смотрящий на то, что он сам только что написал.
reason тоже лжёт: «parou sem concluir nem reportar bloqueio» описывает
молчание, а произошёл сбой с установленной причиной. Тот, кто прочтёт на
панели только reason, решит, что модель заблудилась.
- Доказательство:
session_eventsseq 151 сессии1f94de49. - Нарушенное правило: CLAUDE.md («любой исход сбоя записывает ИСТОЧНИК — infra | modelo | código | política — никогда диагноз методом исключения»), источник — ADR 0020.
U. У терминального исполнителя нет границы проекта (P1)
Внутри контейнера, выполняющего действия, /workspace — это монорепозиторий
самого Brabo, а не worktree проекта, который находится в
/data/project-workspaces/<projectId>/.worktrees/<agentId>.
Dev agent проекта hello-limpo тратил там ходы, думая, что это его проект:
читал apps/engine/mix.exs и даже предлагал
cat lib/engine/actions/git_executor.ex и
sed -n '1,120p' lib/engine/dev/context_builder.ex — git-исполнитель и
построитель контекста платформы, которая его выполняла.
И охват не ограничивается Brabo. for по /data/project-workspaces/*/
перечислил worktree другого проекта (dbd3e508-e0c7-4e29-b134-5d393f518269)
с его коммитами и файлами; следующим шагом агент предложил войти в него,
чтобы прочитать git remote -v и git log. Это было отклонено вручную.
Ничто из этого не злой умысел модели: она ищет собственный проект, а файловая система не говорит, где он кончается. В мультиарендном развёртывании та же команда прочитала бы репозиторий другого клиента.
ADR 0055 проектирует ПОЛИТИЧЕСКУЮ половину этого и прямо говорит, что не решает другую: область путей зависит от того, верно ли правило, а не хватает изоляции — монтирования на проект или контейнера на проект. Эта находка — та половина, которую ADR оставил объявленной открытой.
- Доказательство:
proposed_actionsd1bfeda3(отклонено) и56374def(одобрено, перечислило другой проект) сессии1f94de49.
V. Сессия числится closed, пока исполнение продолжается (P2)
sessions.status сессии 1f94de49 — closed с 23:34:42, через тридцать
секунд после рождения. Исполнение продолжалось до 00:56 — больше часа
агент работал, предлагал действия и тратил токены в сессии, которую база
считает завершённой.
Обе стороны одновременно непоследовательны: UI показывает «Sessão closed — não é possível enviar mensagens» и отрисовывает карточки одобрения, которые работают как обычно. Одобрение в закрытой сессии выполняет настоящую команду.
Это противоречит машине состояний, которую объявляет CLAUDE.md
(created → active → closing → closed | closed_abnormally): closed должен
быть терминальным. Это также отравляет любое измерение по сессиям —
длительность, стоимость и «сколько сессий закончилось хорошо» читают
состояние, не описывающее произошедшее.
Я не исследовал, кто записал closed и почему; это работа фазы, которая
этим займётся, а не триажа.
- Доказательство:
sessions.updated_at= 23:34:42 соstatus = closed;session_eventsтой же сессии до seq 152, в 00:56:46.
Подтверждено в production в этом прогоне
- RN-066 (слепота Arquiteto): 4 вызова
assign_story_modulesвместо 18, ни одного выдуманного имени, 1 module_map вместо 4, каждая история в семантически правильном модуле. Стоимость сессии упала с 7.271 до 3.259 микро. - RN-067 (сессия без процесса): сессия исполнения
dbb84ce8появляется вsessionsИ вengine.session_states— раньше сессияactivate-executionрождалась сиротой.
Прогон проверки Фазы 12 — 2026-08-07
Первый реальный прогон pnpm --filter api validacao:fase-12, незавершённого
пункта 13a.1. Критерий закрылся (выход 0), но только после четырёх
исправлений — трёх в ИНСТРУМЕНТЕ и одного в ПРОДУКТЕ. Исправления
инструмента описаны в validacao-fase-12.md; здесь —
исправление продукта.
W. Dev agent УМИРАЕТ, когда очередь модуля пустеет (P1) — ЗАКРЫТО
При пустой очереди POST /internal/sessions/:id/tasks/claim отвечает 201
с content-length: 0. Сценарий использования возвращает null, но NestJS
сериализует это как ПУСТОЕ тело — Req отдаёт "", а это не nil.
AgentIo.try_claim/2 сопоставлялся с ветвью найденной задачи и вызывал
run_task(""), падая с BadMapError в Map.get("", "id", nil). Поскольку
сервер restart: :temporary, агент умирал окончательно, а Monitor тут же
стирал строку состояния.
Это касается НАСТОЯЩЕГО dev agent, а не только Noop: try_claim/2 живёт в
общем AgentIo. И срабатывает на самом частом исходе — опустевшей очереди.
Эффект — ровно противоположное тому, что дала Фаза 12b: вместо
dev.idle под надзором и пробуждаемого событием — мёртвый процесс.
Набор тестов этого никогда не ловил, потому что fake правильно возвращает
nil. Только реальное исполнение это выявляет — что, буквально, и есть
тезис этой фазы.
ЗАКРЫТО — исправлено на границе (
EngineApiClient.claim_task/4нормализует пустое тело) и закреплено в контракте (AgentIo.try_claim/2принимает""наряду сnil), без изменения HTTP-статуса маршрута. Исключение из заморозки FASE 13 разрешено пользователем по той же причине, что и в Фазе F: без этого измерение было недостижимо. Проверено мутацией.
Реальный прогон с удалённым GitHub (FASE 13b) — 2026-08-07
Первый прогон против настоящего удалённого репозитория (daneiel/test), с
настоящим dev agent и openai/gpt-5-mini. Цепочка до продвижения прошла
целиком; dev agent — нет. Подробности и измерения в
validacao-real.md.
X. Dev agent сжигает потолок итераций в пустом репозитории (P1)
Задача «Expor GET /saudacao» в только что подготовленном репозитории —
только шаблон Gitflow, без кода. Агент потратил все восемь итераций на
search_workspace/read_file, ища «где проект», ни разу не запустил
команду и ни разу не записал файл. Блокировка: limite de iterações atingido, источник modelo, диагноз (nenhum terminal rodado).
Источник modelo технически верен и практически бесполезен: модель не
ошиблась в суждении, она так и не дошла до суждения. Стоимость: 8 вызовов,
205 выходных токенов.
Это первый сценарий, в котором dev agent начинает с абсолютного нуля — все тесты и демо стартовали с workspace, где был код.
Y. search_workspace не отличает «пусто» от «не нашёл» — ЗАКРЫТО
Первые пять вызовов вернули nenhum resultado, и агент прочитал это как
«ищи лучше», а не «здесь ничего нет». Это была исполнимая часть находки X.
ЗАКРЫТО — инструмент стал отвечать по-разному в разных ситуациях. Workspace без единого файла возвращает «o workspace está VAZIO […] CRIE os arquivos necessários (write_file) em vez de continuar procurando»; workspace с файлами возвращает их число, сообщая, что поиск сработал, а не встречается именно термин.
Исправление — это фраза, а не потолок. Агенту не нужно было больше итераций — ему нужно было знать, что искать нечего. И случай из находки (только
.github/иdocs/из bootstrap) — НЕ пустой: есть тест, утверждающий, что там правильный ответ — число, а не указание создать.Новый прогон был сделан, и X НЕ закрылся. Тот же исход —
limite de iterações atingido, 8 вызовов, ни одного PR. Поведение изменилось (один поиск вместо пяти), результат — нет. Гипотеза, что причиной была фраза, оказалась НЕВЕРНОЙ: из восьми итераций семь — исследование, и остаётся одна на то, чтобы написать, закоммитить, запушить и открыть PR.Причиной БЫЛ потолок. С
TOOL_LOOP_MAX_ITERATIONS=25dev agent исследовал, записал ТРИ файла и запустилnpm test— и остановился наdev.awaiting_approval, а не на блокировке. Потолок 8 родился для разговорного агента и не подходит dev agent, которому нужно понять репозиторий прежде, чем действовать (apps/engine/config/runtime.exs:100).X перестаёт быть «сжигает потолок, исследуя» и становится «потолок не тот для этого агента». Исправление продукта — НЕ поднять глобальное значение по умолчанию: Criativo не нужны 25 итераций, чтобы разговаривать. Это потолок по типу агента, и это продуктовое решение: остаётся в триаже.
ЗАКРЫТО в FASE 14d (RN-085). Потолок стал зависеть от типа в
Engine.Harness.Iteracoes:8для разговаривающих,60для dev agent и субагента QA. Критерий повышения — не «много работает», а наличиеtoken_budget_micros, сдерживающего расход, и поэтомуinfra-workflowsиспользует тяжёлый инструмент и остаётся на 8: он работает без бюджета, и для него потолок — единственный существующий тормоз.
Z. Allowlist терминала управляет ГЛАГОЛОМ; область защищает только ПУТЬ
С решённым потолком исполнение стало останавливаться на одобрении терминала. Просьба от 2026-08-06 была «permita sempre comandos desde que seja na pasta do projeto»; ADR 0055 дал ПОТОЛОК (вне папки никогда не автоодобряет), а глагол по-прежнему управляется закрытым списком.
Разрешения npm/pnpm/node/npx оказалось недостаточно: агент запустил
ls -la. Каждый новый глагол попадает в require_approval.
Это не дефект ADR 0055, который никогда не обещал продвигать глаголы, — это разрыв между тем, что просили, и тем, что дали, и он держит лестницу.
Сработало так, как задумано (тоже записать)
Psicólogo сам поставил диагноз, на тяжёлом тарифе, читая event log
провалившегося исполнения, — и назвал обе причины точнее любого
утверждения скрипта: отсутствие терминального tool.call и
search_workspace, вводящий агента в заблуждение. Интроспекция продукта
работает.
AA. Автоодобренный pr_open не имеет учётных данных и всегда падает на удалённом (P1)
ЗАКРЫТО — стало RN-082. Api стала разрешать учётные данные git по OWNER workspace, переиспользуя тот же резолвер RN-058, а не реализуя правило заново. Проверено мутацией.
Обнаружено в 5-м прогоне 13b, первом, где цепочка дошла до PR.
ExecuteGitActionUseCase разрешает git-токен из action.decidedBy —
пользователя, РЕШИВШЕГО действие (execute-git-action.use-case.ts:100). Когда
политика автоодобряет, никто не решает: decided_by остаётся NULL,
accessToken — undefined, и GitHub отвечает Requires authentication.
Контраст в том же запуске — доказательство:
| действие | кто выполняет | учётные данные | исход |
|---|---|---|---|
git_push | engine | подставлены от владельца (git_auth.ex, RN-076) | ✅ executed |
pr_open | api | action.decidedBy → NULL | ❌ failed |
Push дошёл до GitHub — ветка feature/task-d4b36a5b существует на удалённом.
Упал только REST-вызов, открывающий PR.
Это тот же класс, что RN-058 исправило для LLM («ключ, который тратит агент, — ключ OWNER»): путь git в api всё ещё разрешает по «кто решил», а не по «чей workspace».
Практическое последствие: при настроенной автономии — а это режим, ради которого существует Фаза F, — ни один dev agent не может открыть PR у удалённого провайдера. Путь работает, только когда человек кликает по каждому PR, что ровно и есть лестница, объявленная невыполнимой.
AB. Агент GATE не умеет ждать одобрения — превращается в сбой infra (P1)
ЗАКРЫТО — ADR 0057. Агенты gate теперь ПРИОСТАНАВЛИВАЮТСЯ и возобновляются, как dev agent со времён ADR 0052: субагент возвращает
{:awaiting, ...}со всемctx, аQaLeadServerхранит состояние в полёте, подписываетWakeза субагентов и продолжает область с того места, где остановился, когда приходит решение.Пока ожидание длится, область не консолидирует, не выносит вердикт и не блокирует задачу — она ждёт. Отказ тоже возобновляет, с причиной на месте результата.
Заявленное ограничение: перезапуск посреди ожидания теряет цикл, потому что
pendenteживёт в памяти lead. Gate снова запускается черезDispatcher, когда задача возвращается в цикл.
AC. Перенаправление (2>/dev/null) делает любую команду неодобряемой (P1)
ЗАКРЫТО в обеих частях.
parseCommandперестал считать>/>>/<разделителем — они не сцепляют никаких команд. Цель остаётся ТОКЕНОМ сегмента, намеренно: такecho x > /etc/passwdпо-прежнему блокируется потолком области. Глагол стал правильным, а путь не стал свободным./dev/null,/dev/stdin,/dev/stdoutи/dev/stderrперестали считаться пользовательским путём. Список именно такой, и не весь/dev— есть тест, утверждающий, что/dev/sdaпо-прежнему вне области, потому что открытие/devобменяло бы неудобство на дыру.Проверено мутацией в обе стороны: ослабление
/devроняет тест диска, а отключение сцепления роняет 11 тестов.
Обнаружено в 7-м прогоне 13b, после расширения allowlist до 25 глаголов. Агент QA запустил:
ls -la && echo "---" && cat package.json 2>/dev/null; echo "---"; ls *.md 2>/dev/null
Все глаголы были разрешены — ls, echo, cat. И всё равно получилось
require_approval. Прогон чистых функций против команды:
segmentos: [["ls","-la"],["echo","---"],["cat","package.json","2"],["/dev/null"],…]
tokens de caminho: ["/dev/null","/dev/null"]
no escopo? false
parseCommand считает > разделителем сегментов, поэтому 2>/dev/null
становится отдельным сегментом. Это ломается двумя независимыми
способами:
- «глагол» сегмента
/dev/null— сам/dev/null, которого никогда не будет вallow, а составная команда требует, чтобы был разрешён ЛЮБОЙ сегмент; /dev/null— токен АБСОЛЮТНОГО пути вне папки проекта, поэтому потолок RN-075 понижаетauto_approve.
Последствия велики, потому что 2>/dev/null идиоматичен. Модели
постоянно используют его, чтобы заглушить ожидаемую ошибку. На практике
любая команда с перенаправлением вывода неодобряема политикой — проходит
только по клику человека.
Это не нехватка глагола в списке, а форма команды. Расширение allowlist не помогает, и 7-й прогон — доказательство: 25 разрешённых глаголов, и всё равно застряло.
Две отдельные части для триажа:
- сегментация по
>вcommand-matcher.ts(существовавшая ранее и спорная: перенаправление — не композиция команд, как&&и|); - потолок области, считающий
/dev/nullпользовательским путём (path-scope.ts, из Фазы F).
AD. Агент оборачивает команды в bash -lc, и у allowlist нет ответа (P1)
Восьмой прогон 13b, с уже исправленными находками Y, AA, AB и AC. Dev agent сделал ОДИН вызов инструмента:
bash -lc npm test --silent
Глагол — bash. Его нет в allow, и получилось require_approval.
Отказ правильный, и важно, чтобы он был правильным. Разрешение bash
обнулило бы весь allowlist: bash -lc <что угодно> обходит проверку глагола,
включая встроенные deny. Allowlist, принимающий bash, — не allowlist.
Но это замыкает аргумент, который выстраивали прогоны 6, 7 и 8:
| прогон | что застряло | что я сделал |
|---|---|---|
| 6-й | head вне списка | расширил до 25 глаголов |
| 7-й | 2>/dev/null (ФОРМА, не глагол) | исправил парсер и область (AC) |
| 8-й | bash -lc (ВЫЗОВ) | — |
Каждый прогон выявлял новую категорию, и ни одна не была «не хватило глагола». Allowlist глаголов не сходится против агента, который свободно выбирает, как вызвать то, что он хочет запустить, — а три формы (глагол, форма, вызов) — разные пространства, а не пункты одного списка.
Чего это НЕ означает: что allowlist неправилен. Он делает ровно то, что
обещает, и отказ bash доказывает, что граница работает. Чего он не делает
— не делает возможной автономию агента без вмешательства человека.
Два направления для триажа, разных по природе:
- Приостанавливаемые агенты gate (находка AB, открытая половина). Не устраняет одобрение — заставляет агента ЖДАТЬ его, а не умирать, и тогда клик пользователя разблокирует, а не проваливает. Это путь, который ADR 0052 уже открыл для dev agent.
- Политика по профилю агента. Dev agent в изолированном worktree
отличается от агента, касающегося workspace пользователя. Сегодня оба
используют один
permissions.json. Это продуктовое решение с ADR.
AE. Агент QA пытается исправить код, который оценивает (P2)
В финальных прогонах 13b, когда gates на LLM наконец заработали, субагент QA попытался исправить код, который оценивал, — вопреки своей роли. Кто судит, тот не чинит: gate, редактирующий то, что анализирует, перестаёт быть gate и становится ещё одним автором, а вердикт начинает касаться его собственной работы.
У него не получилось, и причина структурная прежде, чем политическая.
Реестр инструментов QA —
[ReadFile, SearchWorkspace, Terminal, EmitQaVerdict]
(apps/engine/lib/engine/gates/qa_tools.ex:10): write_file не существует.
Чтобы писать, ему пришлось бы пройти через terminal — и там он встречает
два барьера, существовавших по другим причинам:
- allowlist глаголов, управляющий тем, что может выполняться (RN-075 и находки Z/AD);
- потолок области путей, понижающий до
require_approvalлюбую команду вне папки проекта.
Это независимые барьеры: ни один не проектировался для этого случая, и именно поэтому сдерживание надёжно — оно не зависит от того, убедит ли промпт модель.
Почему P2, а не P1. Ничего не утекло: поведение было попыткой и было остановлено. Находка фиксирует расхождение между тем, что просит промпт, и тем, что делает модель, и это расхождение — данные: оно говорит, что одна инструкция не удерживает роль, а удерживает структурный барьер.
Что не измерено. Сколько раз пытался, в каком именно прогоне и какой командой: у прогонов 9 и 10 нет записанных подробностей в validacao-real.md, который покрывает до восьмого. Запись находки без этих чисел намеренна — альтернативой было их выдумать, а урок Фазы 10 был как раз в том, что число, записанное по памяти, не считается.
Направление для триажа — то же, что «политика по профилю агента» в
находке AD: правильный инструмент для gate не тот же, что для автора.
Сегодня разница достигается упущением (реестр QA не включает
write_file), что работает, но нигде не объявлено как гарантия — исчезнет в
день, когда кто-нибудь добавит инструмент «для удобства».
Прогоны с ДВУМЯ модулями — 2026-08-08 и 2026-08-09
Две истории, одна в api и одна в web, два dev agents, поднимающихся в
одно и то же время. Такой прогон важен, потому что потолок параллелизма
RN-083 что-то значит только при
по-настоящему независимой работе — с одним модулем Dev Lead отказывается
распараллеливать, и обоснованно.
Было три прогона, и их порядок и есть аргумент:
- 2026-08-08, сломавшийся.
dev-webвзял задачу и умер наfatal: not a git repositoryдо первого хода: ноль потраченных токенов, задача заблокирована, агентidle. Это находка AF ниже. - 2026-08-08, сразу после исправления.
dev-webперешёл с 0 на 16 вызовов, и оба модуля были реализованы параллельно. - 2026-08-09 (проект
9443f1f1, сессия94428b1f), замер. Чистый прогон с уже работающим исправлением, полностью извлечённый черезmedir:execucao: 3m56s, 33 вызова, < US$ 0,01, без перезапуска engine, без немого хода, и оба gate (qaиsecops) одобрили. Служит базовой линией стоимости исполнения двух модулей.
AF. Два dev agents, поднимаясь вместе, портят workspace друг друга (P1)
Engine.Actions.Workspace.ensure!/4 сериализует инициализацию рабочего
дерева по проекту через :global.trans — именно на случай N dev agents,
поднимающихся при активации исполнения. Блокировка была правильной.
Страж, решавший, стоит ли её брать, — нет.
Быстрый путь без блокировки спрашивал, существует ли .git
(apps/engine/lib/engine/actions/workspace.ex:51). А init_from_bare!
начинается с git init, который создаёт .git на первой строке — до
fetch и до checkout. Пришедший в это окно читал «готово», пропускал
блокировку целиком и запускал git worktree add против наполовину
готового репозитория:
fatal: not a git repository
Почему это прошло через десять прогонов незамеченным. Окно существует,
только если второй агент приходит во время инициализации, а активация
поднимает лишь одного агента на модуль. С одной записью в module_map
второго агента никогда не было. Дефект относится к Фазе 14d (параллелизм)
и потребовал первого прогона, который действительно распараллеливал, чтобы
проявиться.
Исправление — метка на диске, .brabo-workspace-pronto, записываемая
только в конце инициализации; быстрый путь начинает спрашивать о ней.
Блокировка не сменила места — сменился критерий «уже готово», который
теперь истинен, только когда это действительно так. Workspace, созданный до
появления метки, принимается и штампуется, но никогда не
переинициализируется: переинициализация стёрла бы незакоммиченную работу.
Чему это учит о тесте, который не поймал. Уже существовал тест на 8
конкурентных ensure/3, и он проходил с багом. Все 8 стартуют
одновременно, все видят пустой каталог, все идут к блокировке — окно никогда
не проверяется. Попытка воспроизвести его по времени тоже не работает:
против локального bare fetch заканчивается слишком быстро, а тест,
гоняющийся за окном, проходит всегда, ничего не доказывая.
Дыру в apps/engine/test/engine/actions/workspace_test.exs закрыло
построение промежуточного состояния вручную вместо попытки его
подгадать по времени: git init в каталоге, без fetch и без checkout, —
ровно то, что видел второй агент. Со старым стражем ensure! возвращает
каталог и никогда не пишет метку — и именно отсутствие метки проверяет
утверждение. Два других теста стерегут миграцию (workspace до метки не
переинициализируется) и реальный случай двух конкурентных ensure!.
Это тот же шаблон, что три пробела самой Фазы 14d, записанный там: проверить деталь — не значит проверить путь к ней. Здесь деталь (блокировка) была правильной и протестированной; неверно решал путь к ней.