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();
}
Три решения внутри решения:
- Проверка только на КЛИЕНТЕ — backend не меняется.
POST .../execution/activateпо-прежнему требуетmaintainer, как всегда требовал. Слияние — чисто вопрос того, СКОЛЬКО кликов UI просит, чтобы прийти в то же состояние, которое уже было достижимо;developer, который изучил бы сеть и вызвал маршрут напрямую, по-прежнему получил бы 403, ровно как сегодня. Новой поверхности авторизации нет — лишь ярлык, который срабатывает только когда результат уже был гарантирован. handleActivateExecutionне дублируется, а ПОВТОРНО ИСПОЛЬЗУЕТСЯ. Та же функция, которую уже вызывает кнопка «Ativar execução» (RN-137, сsessionIdкакoriginSessionId), — которая уже обрабатывает собственную ошибку черезmensagemDaApiи никогда не перебрасывает. Это важно: если бы слияние сработало, а backend отказал по какой-то поздней причине (сессия, ставшая несогласованной между принятием и включением, например),catchвhandleAcceptHandoffНЕ должен показывать «Não foi possível aceitar o handoff» — принятие уже сработало. Повторное использование функции, которая уже проглатывает собственную ошибку (собственный toast), — то, что гарантирует правильное сообщение без дублирования обработки ошибок.- Роль читается из 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, а не функциональный незакрытый вопрос.