ADR 0014 — InfraAgent и живая панель команды (закрытие Фазы 4a)
- Статус: принят
- Дата: 2026-07-24
- Фаза: 4a (сессия 4 — закрытие)
Контекст
Последние два пункта Фазы 4a: InfraAgent (читает module_map + ADR по инфраструктуре от Arquiteto, предлагает Dockerfile/compose/CI через PR — никогда не применяет в окружении) и полная панель команды (все агенты инстанцированы, живой статус через каналы Phoenix, больше не статический/единообразный статус). На этом Фаза 4a закрывается; Фаза 4b (Psicólogo/Anamnese) остаётся на потом.
Критерий приёмки: в подопытном проекте InfraAgent сдаёт PR с валидным Dockerfile, проходящим gates (синтаксический QA + сканирование SecOps), а панель показывает всю команду с правильными состояниями во время реального исполнения.
Решения
1. Handoff Arquiteto→Infra — тот же механизм, новая точка предложения
InfraAgent следует ТОЙ ЖЕ семье агентов уровня сессии
(Criativo/PO/Arquiteto) — а не образцу уровня проекта у Dev/QA/SecOps, —
потому что активируется явным handoff, а не инстанцируется по одному на
модуль. ArquitetoServer получает handle_call(:offer_infra_handoff, ...)
(зеркало confirm_readiness у Criativo): выполняет завершающий ход и
вызывает EngineApiClient.create_handoff(project_id, session_id, "arquiteto", "infra", nil). Выделенный человеческий endpoint
POST projects/:id/sessions/:id/agents/arquiteto/handoff-infra →
OfferInfraHandoffUseCase — не переиспользует endpoint readiness
Criativo (разные агенты и моменты потока).
AcceptHandoffUseCase получает seed автономии, обусловленный
toAgent === "infra": agent_autonomy (infra, open_infra_pr) = auto_approve (ПРЕДЛОЖЕНИЕ PR безопасно автоодобрять — InfraAgent никогда
ничего не применяет в окружении) и (infra, terminal) = deny.
ArquitetoSupervisor/agent-activation.ts уже обобщены по имени агента —
никаких изменений не нужно, кроме того, что agent_command_controller.ex
получает клаузу "infra".
2. Ограничение терминала — структурное + политика, а не новый ActionType
decide() оценивает IAM → agent_autonomy → permissions.json, каждая стадия
лишь ПОВЫШАЕТ разрешительность — deny в agent_autonomy ЗАМЫКАЕТ НАКОРОТКО
до того, как будет запрошен permissions.json. Значит, «запретить общий
терминал, но разрешить hadolint» нельзя (и не нужно) выражать в ТОМ ЖЕ
ActionType terminal: проверка hadolint у InfraAgent (ValidateInfraFile,
инструмент :direct) выполняется как прямой вызов System.cmd через
Engine.Actions.HadolintDetector, без какого-либо proposed_action, — тот же
образец, что у детекторов SecOps. Запрет общего terminal двойной:
структурный (реестр инструментов InfraAgent — [ValidateInfraFile.spec(), ProposeInfraPr.spec()] — никогда не включает Terminal) и политика
(agent_autonomy (infra, terminal) = deny, глубокая защита, проверяемая через
decide(), но не реальный механизм, препятствующий неправомерному
использованию).
3. open_infra_pr обобщает open_adr_pr до N файлов
Новый ActionType (decide.ts), откалиброванный как open_adr_pr
(maintainer/pending по умолчанию). domain/git/infra-pr-execution-result.ts
отражает AdrPrExecutionResult, но paths: string[] (один коммит с
несколькими файлами). ExecuteInfraPrUseCase обобщает ExecuteAdrPrUseCase:
GitProvider.commitFiles уже принимал files: {path,content}[] — только
потребитель был зафиксирован на 1 файле. Фиксированная ветка
feature/infra-setup (одна на сессию, а не по slug, как ADR).
Цикл исправления без нового PR: сессия производит ОДИН-единственный цикл
PR инфраструктуры — исправление после changes_requested переиспользует ТУ ЖЕ
ветку/PR, а не открывает другой. ExecuteInfraPrUseCase определяет это через
InfraArtifactRepository.findBySessionId (новый): если для сессии уже есть
артефакт, вызов трактуется как исправление — только commitFiles, без
createBranch/openPullRequest, без новой строки в infra_artifacts
(переиспользует pullRequestUrl/pullRequestId ИСХОДНОГО исполнения,
прочитанные обратно через listByProjectAndType). InfraAgentServer
получает handle_cast({:correct, findings}, state) (зеркало
DevAgentServer.correct/3): велит модели исправить и снова вызвать
propose_infra_pr — ТОТ ЖЕ инструмент, без особого ветвления для случая
исправления (различение целиком живёт в api).
4. Gates инфраструктуры: лёгкая параллельная таблица, та же машина состояний
За PR инфраструктуры нет task/story/worktree (файлы рождаются как прямое
содержимое, как ADR, — никогда не касаются worktree). Вместо того чтобы
втискивать это в tasks, новая таблица infra_artifacts (миграция 0016) с
ТЕМИ ЖЕ колонками gate, что у tasks (gate_status, gate_correction_count,
blocked, blocked_reason) + pr_action_id (id proposed_action
open_infra_pr — единственный id, который engine знает в ответ). В отличие от
tasks (которая существует ДО PR и открывает gate потом через
OpenGateUseCase), артефакт инфраструктуры рождается только тогда, когда PR
уже открыт, — gate_status приходит сразу 'awaiting_qa'.
RecordInfraGateVerdictUseCase переиспользует ТУ ЖЕ nextGateStatus (чистый
домен, уже агностичный к Task) против InfraArtifactRepository вместо
TaskRepository — с ключом по prActionId, а не taskId
(findByPrActionId). Комментарий в PR через listByProjectAndType (а не
findInSessionForUpdate, предназначенный исключительно для использования
внутри транзакции с блокировкой).
Engine.Infra.InfraGateRunner (новый) ДЕТЕРМИНИРОВАН от начала до конца
(без собственного GenServer, без LLM) — run_qa/3 запускает hadolint по
каждому Dockerfile из payload PR (получаемому через новый
GetInfraPrFilesUseCase + endpoint GET .../infra-artifacts/:prActionId/files,
поскольку нет worktree, на который можно указать детекторам);
run_secops/3 запускает gitleaks+semgrep (те же детекторы, что у SecOps
для dev) против ВРЕМЕННОГО каталога, записанного на лету с файлами из payload
(удаляется в after у try). Запускается через
Engine.Gates.Dispatcher.run_infra_qa/3/run_infra_secops/3 (та же
тестируемая косвенность, что Dev↔Gates, но с Task.start — fire-and-forget —
вместо собственного GenServer, поскольку runner не нуждается в состоянии между
вызовами).
5. Живая панель команды — broadcast в существующих точках выпуска
Без нового outbox-relay (большее, ненужное для критерия приёмки изменение):
Engine.Sessions.LiveBroadcast.event_appended/4 (новый, маленький)
вызывается рядом с EngineApiClient.append_event во ВСЕХ GenServer уровня
проекта, которые лишь писали событие без broadcast (DevAgentServer,
QaAgentServer, SecOpsAgentServer, InfraAgentServer,
InfraGateRunner), — broadcast в канал session:<id>, событие
event.appended. Разговорные агенты (Criativo/PO/Arquiteto/Infra) получают
agent.status (working/idle) на существующих границах хода (начало/конец
каждого handle_cast/handle_call, выполняющего run_turn).
Web: session-channel.ts получает onEvent/onAgentStatus — при получении
queryClient.invalidateQueries(['session-events', ...]) (переиспользует уже
существующий парсинг/кэш polling, лишь ускоряет refetch).
ProjectOverviewTab открывает СОБСТВЕННЫЙ канал (connectSessionHeartbeat),
независимый от того, что уже открывает SessionPage, поскольку это разные
маршруты/вкладки, монтируемые в разные моменты.
lib/agent-status.ts (новый) — deriveAgentRoster(events, moduleMap, executionActivated, handoffs) собирает РЕАЛЬНЫЙ состав:
criativo/po/arquiteto всегда; dev-<modulo> для каждого модуля module_map,
когда исполнение активировано (синтезирует AgentDef, переиспользуя
значок/цвет общего «dev-backend», так как для динамического id нет
фиксированной записи); qa/secops, когда какой-либо gate (dev ИЛИ infra) хоть
раз открывался; infra, когда handoff принят. Статус: разговорные агенты —
через последний agent.status; dev — через эвристику по последним
собственным событиям (dev.*/backlog.task_blocked); qa/secops — через
gateStatus самого свежего заключения (awaiting_qa/awaiting_secops
говорит, у кого из двух мяч, — они синглтоны на проект, обрабатывают
последовательно). «Команда агентов» в ProjectOverviewTab начинает
использовать это, с model/autonomy (AgentCard, уже принимал props),
разрешаемыми через getAgentModelBinding/listAgentAutonomy один раз на
загрузку страницы.
6. Лента активности и вкладка Одобрения
activity.ts получает ветки infra.gate_changed, infra.pr_opened/
infra.pr_failed, infra.artifact_blocked и
architecture.readiness_confirmed (зеркало уже существующих веток
pr.gate_changed/adr.*, текст ясно говорит «PR инфраструктуры»).
artifact.qa_verdict/secops_verdict (уже существующая ветка,
переиспользуемая dev И infra) проверяет prActionId в payload, чтобы
различать текст, не дублируя ветку.
Новый человеческий endpoint GET projects/:id/infra-artifacts
(ListInfraArtifactsUseCase) + хук useInfraArtifacts. У PrGateTimeline
(уже существующий компонент, используемый PR dev) prop task: Task сужен до
структурного интерфейса GateSubject { title, blocked, blockedReason, gateStatus } — и Task, и InfraArtifact удовлетворяют ему структурно, не
привязывая компонент к домену backlog. Новый раздел «PR инфраструктуры на
ревью» во вкладке Одобрения, тот же компонент.
Последствия
- Тесты:
decide.spec.tsдоказывает короткое замыкание (deny в agent_autonomy никогда не доходит до запроса permissions.json, даже при широком allow); интеграцияProposeActionUseCase(InfraAgent, предлагающийterminal, получаетdenied);RecordInfraGateVerdictUseCase(полное зеркало тестов gate для task, включая уже заблокированный артефакт, отклоняющий новое заключение). Engine:ValidateInfraFile(отсутствие hadolint не ломает ход),InfraAgentServer(счастливый путь с N файлами, исправление, deltas +agent.status, регидрация),InfraGateRunner(lint с находкой, одобрение запускает следующий gate, отсутствующий сканер пропускается без поломки). docker build --checkостаётся вне рамок — потребовал бы монтирования сокета хоста в контейнер engine (Alpine, сегодня без docker CLI/демона), реальный риск, не принимаемый без явного запроса. Только hadolint (Engine.Actions.HadolintDetector, необязательный бинарник, тот же образец, что у остальных детекторов).- «Живая» панель через broadcast в уже существующих точках выпуска — без нового обобщённого outbox-relay api→engine→Phoenix.
- Фаза 4b (Psicólogo/Anamnese) остаётся вне.
Рамки и допущения
Сессия производит ОДИН цикл PR инфраструктуры (исправления переиспользуют ту же ветку/PR — никогда второй конкурирующий PR). Без глубокой корреляции diff↔находка сканера (то же упрощение, что в ADR 0013). Статус dev/qa/secops на панели эвристический (best-effort по event log, а не транзакционный источник истины) — достаточно для панели, не используется никаким решением gate.