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

ADR 0069 — Условное слияние принятия handoff с включением выполнения

  • Статус: принят
  • Дата: 2026-08-13
  • Контекст: запрос пользователя — сократить с двух кликов до одного, когда тот, кто принимает handoff к Dev Lead, уже имеет роль для включения выполнения
  • Расширяет: ADR 0053 (Dev Lead как адресуемый агент); RN-135/RN-136/RN-137 в docs/business-rules.md (карточка handoff и ярлык «Ativar execução», которые это решение начинает связывать)

Контекст​

У карточки handoff к Dev Lead в SessionPage.tsx сегодня две кнопки: «Aceitar handoff e iniciar dev-lead» (требует developer) и «Ativar execução» (требует maintainer, RN-137). Это два клика, потому что это два разных РАЗРЕШЕНИЯ, намеренно не выровненных: тот, кто включает выполнение, становится session.createdBy сессии выполнения, и именно эту роль ProposeActionUseCase/ResolveEffectiveRoleUseCase определяют как ЭФФЕКТИВНУЮ для каждого git_commit/git_push/pr_open, которые dev agents будут предлагать с этого момента (ExecutionController#activate, RequireRole('maintainer')). Включение под developer дало бы dev agents, неспособных открыть PR, — исходное обоснование того, чтобы оставить два маршрута с разными требованиями, и оно остаётся полностью в силе.

Запрос не в том, чтобы снизить это требование. А в том, что когда у того, кто на экране, УЖЕ есть maintainer/owner — и, следовательно, он уже мог бы нажать обе кнопки подряд, не наткнувшись ни на один 403, — второй клик ничего не защищает: он лишь повторяет решение, которое роль пользователя уже разрешает. Для того, у кого есть лишь developer, второй клик остаётся единственным способом сигнализировать «да, я хочу, чтобы ЭТО стало сессией выполнения» — он не может исчезнуть, потому что тот никогда не смог бы включить, даже нажав.

Решение​

handleAcceptHandoff (SessionPage.tsx) автоматически связывает activateExecution, когда toAgent === 'dev-lead' И ЭФФЕКТИВНАЯ роль принимающего — прочитанная из ТОГО ЖЕ useCurrentWorkspaceWithRole(), который уже разрешает «Auto mode» (RN-153) и экраны Одобрений/Настроек, — owner или maintainer. Для developer (или ещё не определённой роли) текущий поток остаётся НЕТРОНУТЫМ: принятие ничего не включает, а «Ativar execução» остаётся второй кнопкой, пока карточка на экране.

if (toAgent === 'dev-lead' && podeFundirHandoffComExecucao) {
await handleActivateExecution();
}

Три решения внутри решения:

  1. Проверка только на КЛИЕНТЕ — backend не меняется. POST .../execution/activate по-прежнему требует maintainer, как всегда требовал. Слияние — чисто вопрос того, СКОЛЬКО кликов UI просит, чтобы прийти в то же состояние, которое уже было достижимо; developer, который изучил бы сеть и вызвал маршрут напрямую, по-прежнему получил бы 403, ровно как сегодня. Новой поверхности авторизации нет — лишь ярлык, который срабатывает только когда результат уже был гарантирован.
  2. handleActivateExecution не дублируется, а ПОВТОРНО ИСПОЛЬЗУЕТСЯ. Та же функция, которую уже вызывает кнопка «Ativar execução» (RN-137, с sessionId как originSessionId), — которая уже обрабатывает собственную ошибку через mensagemDaApi и никогда не перебрасывает. Это важно: если бы слияние сработало, а backend отказал по какой-то поздней причине (сессия, ставшая несогласованной между принятием и включением, например), catch в handleAcceptHandoff НЕ должен показывать «Não foi possível aceitar o handoff» — принятие уже сработало. Повторное использование функции, которая уже проглатывает собственную ошибку (собственный toast), — то, что гарантирует правильное сообщение без дублирования обработки ошибок.
  3. Роль читается из WORKSPACE, а не из нового endpoint проекта. То же приближение, что уже используется в ProjectApprovalsTab.tsx/ ProjectSettingsTab.tsx/«Auto mode» этого же экрана: сегодня не существует разрешённой на клиенте роли ПРОЕКТА, поэтому вопрос «я maintainer?» уже решался так до этого изменения — это не новый источник, а источник, который уже разрешал показ других эквивалентных элементов управления.

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

  • Всегда сливать и позволить единственному клику проваливаться с 403 для developer. Отвергнуто: у карточки была бы одна кнопка, которая для половины ролей делала бы вызов, ОБРЕЧЁННЫЙ на провал, за принятием, которое сработало, — пользователь увидел бы «Papel insuficiente» после того, как уже успешно принял handoff, — запутанное сообщение, привязанное к действию, которое на самом деле удалось. UX хуже, чем сохранение двух кнопок для тех, кому они нужны.
  • Спрашивать («Включить также выполнение?») вместо молчаливого связывания. Отвергнуто решением пользователя: для maintainer/owner два сегодняшних клика УЖЕ являются явным и избыточным согласием — второе подтверждение не защищает решение, которое человек уже принял дважды (принять handoff, затем нажать включить). Спрашивать снова было бы трением без выигрыша в безопасности.
  • Снизить требование POST .../execution/activate до developer, устранив различие под корень. Отвергнуто — это ровно то, что RN-137 уже решила НЕ делать, потому что это молча перевернуло бы разрешение ЭФФЕКТИВНОЙ роли PR, которые открывают dev agents (каждый PR переходил бы с auto_approve на require_approval всякий раз, когда включающий был бы developer, без того, чтобы кто-то явно это решил). Это слияние не открывает этот вопрос заново — оно лишь избегает избыточного клика для тех, у кого уже были обе роли.
  • Определять эффективную роль ПРОЕКТА (а не workspace) для решения о слиянии. Отвергнуто, потому что сегодня этого не существует: у клиента нет маршрута, возвращающего эффективную роль по ПРОЕКТУ (ближайшее — область agent/area из ADR 0064, которая касается модели LLM, а не RBAC). Вводить такой маршрут только ради этого UX-решения было бы неправильной причиной — useCurrentWorkspaceWithRole() уже является приближением, используемым везде в эквивалентных местах этого же экрана.

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

  • developer не получает ярлыка — и это намеренно, а не пробел. Ему по-прежнему нужен maintainer/owner, чтобы включить выполнение потом, ровно как сегодня. Никто не теряет возможности: тот, кто лишь принимал handoff, продолжает принимать.
  • Ноль изменений контракта в backend. ExecutionController#activate остаётся с RequireRole('maintainer') без изменений; это решение целиком в apps/web.
  • На один клик меньше — значит, сессия выполнения быстрее приходит к правильному originSessionId. RN-135 (сессия чата закрывается, когда выполнение стартует этим путём) для maintainer/owner начинает происходить в тот же момент, что и принятие, — без окна между двумя кликами, в котором сессия-источник оставалась active, ожидая второго.
  • Кнопка «Ativar execução» не была удалена из карточки. Она была бы избыточной для того, у кого есть роль (слияние уже сделало работу), и без эффекта для того, у кого нет (последует 403), но условное удаление по роли открыло бы лишнюю поверхность состояний для тестирования без явной выгоды — вся карточка и так исчезает, как только handoff перестаёт быть offered (принят), поэтому окно, в котором кнопка «лишняя» видима и инертна, на практике — это время между кликом и инвалидацией запроса handoffs, а не функциональный незакрытый вопрос.