ADR 0048 — Решение в event log и порядок gate
Контекст
Две находки dogfooding, которые Фаза 12 оставила зафиксированными и не исправленными. Пересмотрены вместе, потому что это один класс дефекта: важный факт происходил и не записывался нигде, где мог бы служить памятью.
Находка #17 — решения по действию не существовало в log
proposed_action.created, .approved и .denied шли только в outbox.
Outbox — это транспорт: он сливается, помечается processed_at и
обрезается. Решение сохранялось лишь в proposed_actions.decided_at — столбце, который говорит
КОГДА, но не появляется в хронологии, которую читают UI, Psicólogo и Anamnese.
Два последствия, и второе стало видно только при написании этого ADR:
- Главную метрику Фазы 10 не удалось собрать. «Клики одобрения» были центральным столбцом таблицы наблюдения dogfooding, и не было запроса, который бы её произвёл.
docs/reference/events.mdвсегда документировал эти три как доменные события. Документация обещала то, чего код не делал, — худший вид ошибки документации, потому что у читающего нет способа заподозрить.
D5 и дефект, который был под ним
ADR 0045 зафиксировал D5 как
известное ограничение: при автономии dev в require_approval агент
перерабатывал worktree, и ожидающее одобрение выполнялось против пути, которого
уже не было. Он предсказал, что правильным исправлением будет «worktree на task».
При исследовании для исправления картина оказалась иной и хуже:
AgentIo.propose/3 отбрасывал статус действия ({:ok, _action} -> :ok).
Агент предлагал commit, push и PR и вызывал open_gate + run_qa
безусловно, не зная, выполнилось ли что-нибудь. При
ручной автономии:
- три действия рождаются
pending; - gate всё равно открывается, и QA сканирует worktree — где находятся файлы — и одобряет;
- SecOps одобряет, task становится
done; task.gate_resolvedосвобождает агента, который берёт следующую и удаляет worktree;- пользователь одобряет commit, и
git add -Aвыполняется в исчезнувшем каталоге.
Шаг 5 возвращает {"", 2} — System.cmd с несуществующим cd не бросает
исключения, — а git/2 превращает это в {:error, ""}: действие проваливается с
пустой диагностикой.
Реальный ущерб — не провал одобрения. Это закрытие task как выполненной без единой
закоммиченной строки и без какого-либо PR. И это не требовало экзотической
конфигурации: переключатель панели предоставляет require_approval для dev-*.
Решение
Решение по действию становится доменным событием, с реальным актором
proposed_action.created (актор = агент, который предложил), .approved и .denied
(актор = пользователь, который решил) начинают записываться в session_events,
рядом с уже существовавшими строками outbox. Outbox остаётся
транспортом; log становится памятью.
Payload .created несёт итоговый status, и именно это делает автоодобрение
проверяемым: подсчёт событий .approved считает ЧЕЛОВЕЧЕСКОЕ решение;
политика, решающая сама, появляется в .created со status: auto_approved
и актором-агентом и никогда не путается с кликом. Именно этого
различия не хватало для метрики Фазы 10.
approve_always входит бесплатно: он делегирует ApproveActionUseCase.
Намеренно остался вне proposed_action.created, который
provision-repository и bootstrap-runner испускают прямо в outbox: те
действия — мутации bootstrap, уже описанные bootstrap.step_* в той же
сессии, и их дублирование посчитало бы один факт дважды в метрике
одобрения.
Gate открывается только после открытия PR
AgentIo.propose/3 начинает возвращать :executed | :pending | :refused.
Агент читает три исхода и:
- все выполнены (автономия
auto_approve, значение по умолчанию, которое засевает включение) — открывает gate, как всегда; - какое-то ожидает — переходит в
:awaiting_approval, удерживая worktree, и не открывает никакого gate. Без PR нечего оценивать.
Освобождает агента task.pr_settled, испускаемый api, когда у pr_open
есть исход — выполнен, отклонён или провален. opened: true открывает gate, поздно и
правильно; opened: false возвращает task с диагностикой, вместо того чтобы оставить
агента вечно ждать gate, который никто не откроет.
Три вещи сделали это дешёвым:
propose_prуже нёсstoryTaskIdв payload, поэтому api точно знает, какую task открыл PR, без новой таблицы и без join.aggregateType: 'task'уже сливаетсяEngine.Outbox.Drainс Фазы 12b — ни нового типа агрегата, ни нового worker, лишь одна клауза вDevAgentWakeWorker.dev_agent_states.status—:string, а не enum — новое состояние не потребовало миграции.
Отклонённый PR не учитывается в circuit breaker. Решение принял пользователь; агент не сжёг ни одного потолка. Это тот же принцип, что уже действовал для восстановления после перезапуска (RN-047).
Почему НЕ worktree на task
ADR 0045 предсказал worktree-на-task как исправление D5. Предсказание не сбылось, и причина в том, что оно исправляло не тот симптом: worktree на task не даёт каталогу исчезнуть, но gate продолжал бы оценивать PR, которого не существует, и task продолжала бы закрываться без commit. Дефект был не в удалении — а в том, что gate открывался слишком рано.
При исправленном порядке D5 умирает как следствие: worktree перерабатывается лишь
в gate_resolved, gate открывается лишь после открытия PR, а PR открывается лишь после
выполнения commit и push. Ни одно ожидающее действие не переживает собственный
worktree, без изменения структуры каталогов, без роста диска и
без новой политики очистки.
Последствия
docs/reference/events.md перестаёт лгать о трёх типах событий — а
генератор, сравнивающий прозу с точками испускания, теперь находит все три
по-настоящему.
Метрика, которую потеряла Фаза 10, начинает существовать. Следующий dogfooding сможет ответить на «сколько раз решал человек» запросом, а не живой записью — именно она была потеряна, потому что не была сделана (см. сбор урожая).
Объём session_events растёт: каждое предложенное действие порождает ещё одно событие.
Это действия, которые уже описывались при выполнении (action.executed/action.failed);
не хватало начала и решения.
Значение по умолчанию тестового fake engine сменилось с pending на auto_approved.
Это не имело значения, пока статус отбрасывался; отсюда имеет, и
auto_approved — то, что производит реальность: ActivateExecutionUseCase засевает
auto_approve для git-действий каждого dev agent. Значение по умолчанию, отличное от
production, отправило бы весь набор тестов по пути ручного одобрения.
Остаётся на потом, как backlog:
- Worktree на task, если однажды появится случай, когда одобрение длится достаточно долго, чтобы worktree мешал по другой причине. Это перестало быть исправлением дефекта и стало архитектурным выбором.
git/2с пустой диагностикой.System.cmdс несуществующимcdвозвращает{"", 2}, и это становится{:error, ""}при любом сбое каталога, а не только в этом. Коренная причина этого закрыта, поэтому пустое сообщение перестало иметь известный триггер — но дыра в диагностике остаётся.- Ожидающие действия типов, отличных от
pr_open. Блокировка покрывает путь dev agent, где ущерб был конкретным. Ожидающийterminalпока ничего не блокирует, и в этом не было нужды.