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

ADR 0088 — Staff: код готов, спящий для автоматического запуска

  • Статус: Принят
  • Дата: 2026-08-16
  • Контекст: docs/fluxo.yml, блок id: staff (camada_decisao_tecnica), status: planned со времён ADR 0085
  • Прямой прецедент: ADR 0053 (Dev Lead — одиночный разговорный агент, без области), ADR 0068 (артефакт без таблицы, версионируемый в event log)

Контекст​

docs/fluxo.yml объявляет Staff/Principal Engineer со времён ADR 0085 как status: planned: «контракт готов, включение решено, ждёт триггера». Задуманный для него триггер — Anamnese, замечающая ПОВТОРЯЮЩУЮСЯ системную проблему, — но Anamnese приостановлена (ANAMNESE_ENABLED=false, продуктовое решение от 2026-08-10) и не предложит никакого handoff, пока это длится.

Владелец продукта решил заранее подготовить КОД, зная, что автоматический триггер не сработает: Staff готов и доступен для ручного вызова (любой агент или пользователь может адресовать ему handoff уже сегодня через общий внутренний маршрут), и в ожидании остаётся только автоматический запуск. Это НЕ полуреализация — это то же различие, которое ADR 0081 уже провёл для жизненного цикла контейнера (таблица готова, оркестратора ещё нет): недостающая часть объявлена, а не спрятана.

Решение​

Staff появляется как ПЯТЫЙ одиночный разговорный агент (рядом с Criativo, PO, Arquiteto и Dev Lead), в точности повторяя шаблон Arquiteto — GenServer на сессию, rehydration из event log, собственный ограниченный цикл tool use (потолок 14, тот же калибр, что у Arquiteto/Dev Lead: рассуждение, а не лёгкая беседа) — с двумя намеренными отличиями.

1. Без kickoff/1​

Все остальные разговорные lead синтезируют начальную инструкцию из event log сессии при СВЕЖЕМ старте (kickoff_instruction/1 у каждого) — всегда есть предыдущий артефакт для пересказа (product_brief, module_map, backlog). У Staff такого источника нет: системная проблема, которая выносит его на поверхность, рождается снаружи (Anamnese, когда она появится, или тот, кто создаёт handoff вручную сегодня), а не из event log самой сессии. Без kickoff Engine.Agents.StaffSupervisor.start_agent/2 поднимает процесс (регидрирует историю), и он простаивает до первого user_message — которым тот, кто адресовал handoff, объясняет проблему.

apps/engine/lib/engine_web/controllers/agent_command_controller.ex отражает это: клауза start/2 для "staff" НИКОГДА не вызывает StaffServer.kickoff/1 (такой функции даже нет), в отличие от po/arquiteto/dev-lead/infra, которые вызывают её условно, когда origin == :started.

2. Включение: ОБЩИЙ путь, без USER_STARTED_AGENTS​

USER_STARTED_AGENTS (apps/api/src/domain/sessions/agent-activation.ts) — исключение для Criativo: он стартует БЕЗ handoff, по прямой команде пользователя. Исследование подтвердило, что Staff НЕ нужно вносить в этот список: canActivateAgent уже включает любого агента с accepted handoff, адресованным ему, — тот же путь, что уже действует для dev-lead/arquiteto/infra сегодня. Для этого в api не понадобилось никаких изменений домена:

  • assertHandoffTargetAllowed (apps/api/src/domain/agents/agent-areas.ts) лишь ОТКАЗЫВАЕТ handoff, адресованному СУБАГЕНТУ области (ADR 0038); у Staff нет области — он проходит свободно, как Criativo/PO/Arquiteto/Dev Lead.
  • ActivateAgentUseCase/ApiToEngineClient.startAgent обобщены по agent: string — никакого enum расширять не нужно.

Следствие честное: «доступен вручную» означает, что доменная МЕХАНИКА допускает handoff и включение, а не что сегодня существует отдельный экран, на котором человек выбирает «адресовать handoff Staff» иначе, чем через код другого агента или прямой вызов внутреннего маршрута (POST .../internal/sessions/:sessionId/handoffs) — общий UI «ручного handoff агенту на выбор» остаётся в backlog (docs/explanation/backlog.md), как было и до этого изменения.

3. propose_rfc — единственный инструмент, без proposed_action​

Engine.Agents.StaffTools.propose_rfc записывает artifact.rfc_staff (проблема, варианты с компромиссами, рекомендация, одноразовый PoC) и возвращает handoff Arquiteto в ТОМ ЖЕ tool call — без подтверждения человеком посередине, тот же шаблон, что у CriativoServer, испускающего product_brief и предлагающего handoff PO в том же ответе.

Механизм определили два реальных прецедента, и поэтому НИ ОДНОГО нового сценария использования в api не появилось:

  • Engine.Harness.Tools.EmitInsight (emit_insight у Arquiteto) — это шаблон артефакта БЕЗ вывода: весь payload приходит из tool call, а событие записывается напрямую через EngineApiClient.append_event_returning/3 — без таблицы, без выделенного сценария использования.
  • artifact.c4_diagram (ADR 0068) — КОНТРАСТ, подтверждающий выбор: у него есть собственный сценарий использования в api, потому что уровень Container ВЫВОДИТСЯ из действующего module_map — эти данные есть только у api. RFC ничего не выводит с той стороны.

propose_rfc также НЕ является proposed_action: запись архитектурного документа — это не внешний эффект (не git, не терминал и не расход агента) — реальное решение (принять, адаптировать, отказаться) принимает Arquiteto, в handoff, который инструмент уже возвращает. То же рассуждение, что и для emit_insight.

Последствия​

За

  • Staff существует от начала до конца в engine (сервер, инструмент, supervisor, маршрутизация) и тестируем уже сегодня, не дожидаясь выхода Anamnese из доработки — когда она вернётся, останется лишь, чтобы КОД Anamnese вызвал create_handoff(..., "staff", ...); остальное уже готово.
  • Для включения не понадобилось никаких изменений домена в api — исследование доказало, что общего пути уже достаточно, и это позволило не открывать новое исключение (USER_STARTED_AGENTS), которое потом пришлось бы отменять.
  • docs/fluxo.yml перестаёт быть единственным источником, знающим о существовании Staff: код совпадает с тем, что реестр уже объявлял, а реальный пробел (автоматический триггер) описан точно, а не обобщённо («ждёт триггера»).

Против, объявлено

  • Список участников панели команды (apps/web/src/lib/agent-status.ts) и карточка dashboard (ProjectCardSummary.roster, через apps/api/.../projects-summary.repository.ts) показывают Staff, когда он активен — staffActive добавлен в оба пути по тому же критерию, что и infraActive, чтобы не повторить расхождение, о котором уже предупреждал комментарий RosterFacts («два правила разойдутся на первом новом агенте»).
  • Экран Сессии (SessionPage.tsx) НЕ был затронут. staff не входит в AGENTES_DE_CHAT — тот же шаблон, уже принятый для infra (агент НАСТОЯЩИЙ, АКТИВНЫЙ и тоже вне этого списка: комментарий самого файла документирует, что handoff к Infra «никогда не принимается ЗДЕСЬ»). Это не НОВЫЙ пробел Staff: это шаблон, который у продукта уже был для lead без диалога, ведомого через composer сессии. Реальное отличие объявлено: у Infra есть kickoff/1 (работает сам после принятия); у Staff нет — поэтому без чат-поверхности, которая отправит ему первый user_message, сквозной путь использования сегодня — это внутренний маршрут (POST .../agent/message, agent: "staff"), а не экран. Закрыть это значит затронуть SessionPage.tsx, самый оспариваемый файл продукта, и это не входило в это решение — подготовить заранее код, а не UI чата.
  • Нет записи в Engine.Harness.Agents.identity/1 (apps/engine/lib/engine/harness/agents.ex) — то же состояние, что у dev-lead сегодня, который тоже падает в общий fallback («Você é o agente staff.»). Это не НОВЫЙ пробел: это реальное состояние активного прецедента, не исправленное мимоходом.

Рассмотренные альтернативы​

Ждать выхода Anamnese из доработки, чтобы построить Staff. Отвергнуто — явное решение владельца продукта: заблаговременный код устраняет «второй PR» в день, когда Anamnese снова включат, а ценность РУЧНОГО вызова существует независимо от автоматического триггера.

Добавить staff в USER_STARTED_AGENTS. Отвергнуто — исследование показало, что это не нужно: стандартный путь canActivateAgent уже включает по принятому handoff, а этот список существует для ПРОТИВОПОЛОЖНОГО исключения (агента, который стартует БЕЗ handoff). Добавление сюда выдумало бы правило, которого домен не требует.

Дать Staff kickoff/1 с общей инструкцией («исследуй системные проблемы»). Отвергнуто — без реального артефакта для пересказа (а его будет производить только Anamnese) kickoff был бы пустым приглашением; сессия простаивает до первого сообщения, и честно, что так и есть.

Ссылки​

  • docs/fluxo.yml — блок id: staff
  • docs/business-rules.md — RN-305/RN-306
  • ADR 0053 — шаблон одиночного разговорного агента без области
  • ADR 0068 — контраст, решающий «без выделенного сценария использования» для RFC
  • ADR 0081 — прецедент «деталь готова, реального потребителя ещё нет», объявленный без смягчения
  • apps/engine/lib/engine/agents/staff_server.ex, staff_tools.ex, staff_supervisor.ex
  • apps/engine/lib/engine_web/controllers/agent_command_controller.ex
  • apps/api/src/domain/sessions/agent-activation.ts, apps/api/src/domain/agents/agent-areas.ts
  • apps/web/src/lib/agents.ts, apps/web/src/lib/agent-status.ts