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

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:

  1. Главную метрику Фазы 10 не удалось собрать. «Клики одобрения» были центральным столбцом таблицы наблюдения dogfooding, и не было запроса, который бы её произвёл.
  2. 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 безусловно, не зная, выполнилось ли что-нибудь. При ручной автономии:

  1. три действия рождаются pending;
  2. gate всё равно открывается, и QA сканирует worktree — где находятся файлы — и одобряет;
  3. SecOps одобряет, task становится done;
  4. task.gate_resolved освобождает агента, который берёт следующую и удаляет worktree;
  5. пользователь одобряет 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 пока ничего не блокирует, и в этом не было нужды.