ADR 0069 — Conditional merge of handoff acceptance with execution activation
- Status: accepted
- Date: 2026-08-13
- Context: user request — reduce two clicks to one when whoever accepts the handoff to the Dev Lead already has the role needed to activate execution
- Extends: ADR 0053 (the
Dev Lead as an addressable agent); RN-135/RN-136/RN-137 in
docs/business-rules.md(the handoff card and the "Activate execution" shortcut that this decision now chains)
Context
The handoff card to the Dev Lead, in SessionPage.tsx, currently has two
buttons: "Accept handoff and start dev-lead" (requires developer) and
"Activate execution" (requires maintainer, RN-137). They are two clicks
because they are two different AUTHORIZATIONS, deliberately not aligned:
whoever activates execution becomes session.createdBy of the execution
session, and it's that role that ProposeActionUseCase/
ResolveEffectiveRoleUseCase resolve as the EFFECTIVE role for every
git_commit/git_push/pr_open that the dev agents propose from then on
(ExecutionController#activate, RequireRole('maintainer')). Activating as
developer would leave dev agents unable to open PRs — the original
justification for keeping the two routes with different requirements, and it
still fully holds.
The request isn't to lower that requirement. It's that, when whoever is on
the screen ALREADY has maintainer/owner — and could therefore already
click both buttons in sequence without hitting a single 403 — the second
click protects nothing: it just repeats a decision the user's role already
authorizes. For someone with only developer, the second click remains the
only way to signal "yes, I want THIS to become the execution session" — it
can't disappear, because that person would never be able to activate even by
clicking.
Decision
handleAcceptHandoff (SessionPage.tsx) chains activateExecution
automatically when toAgent === 'dev-lead' AND the EFFECTIVE role of whoever
accepts — read from the SAME useCurrentWorkspaceWithRole() that already
authorizes "Auto mode" (RN-153) and the Approvals/Settings screens — is
owner or maintainer. For developer (or a role not yet resolved), the
current flow stays UNTOUCHED: accepting doesn't activate anything, and
"Activate execution" remains as a second button while the card is on
screen.
if (toAgent === 'dev-lead' && podeFundirHandoffComExecucao) {
await handleActivateExecution();
}
Three decisions within the decision:
- The check happens only on the CLIENT — the backend doesn't change.
POST .../execution/activatecontinues requiringmaintaineras it always has. The merge is purely a matter of HOW MANY clicks the UI asks for to reach the same state that was already reachable; adeveloperwho inspected the network and called the route directly would still get a 403, exactly as today. There's no new authorization surface — only a shortcut that only fires when the outcome was already guaranteed. handleActivateExecutionisn't duplicated, it's REUSED. The same function the "Activate execution" button already calls (RN-137, withsessionIdasoriginSessionId) — which already handles its own error withmensagemDaApiand never re-throws. This matters: if the merge fired and the backend refused for some late reason (a session that became inconsistent between acceptance and activation, for instance), thecatchinhandleAcceptHandoffmust NOT show "Could not accept handoff" — the acceptance had already succeeded. Reusing the function that already swallows its own error (its own toast) is what guarantees the correct message without duplicating error handling.- The role is read from the WORKSPACE, not from a new project
endpoint. Same approach already used in
ProjectApprovalsTab.tsx/ProjectSettingsTab.tsx/the "Auto mode" toggle on this same screen: there is no PROJECT role resolved on the client today, so the question "am I a maintainer?" was already answered this way before this change — it's not a new source, it's the source that already authorized the display of other equivalent controls.
Alternatives considered and discarded
- Always merge, and let the single click fail with 403 for
developer. Discarded: the card would have a single button that, for half the roles, would make a call DOOMED to fail behind an acceptance that had already worked — the user would see "Insufficient role" after having already successfully accepted the handoff, a confusing message tied to an action that actually succeeded. Worse UX than keeping the two buttons for whoever needs them. - Ask ("Do you also want to activate execution?") instead of chaining
silently. Discarded by user decision: for
maintainer/owner, today's two clicks ALREADY are explicit, redundant consent — the second confirmation doesn't protect a decision the person already made twice (accepting the handoff, then clicking activate). Asking again would be friction with no security gain. - Lower the requirement of
POST .../execution/activatetodeveloper, eliminating the distinction at the root. Discarded — that's exactly what RN-137 had already decided NOT to do, because it would silently invert the EFFECTIVE role resolution of the PRs that dev agents open (every PR would go fromauto_approvetorequire_approvalwhenever whoever activated wasdeveloper, without anyone having explicitly decided that). This merge doesn't reopen that question — it only avoids a redundant click for whoever already had both roles. - Resolve the effective PROJECT role (not workspace) to decide the
merge. Discarded because it doesn't exist today: the client has no route
that returns the effective role BY PROJECT (the closest thing is the
agent/areascope from ADR 0064, which is about the LLM model, not RBAC). Introducing that route just for this UX decision would be the wrong cause —useCurrentWorkspaceWithRole()is already the approach used everywhere equivalent on the same screen.
Consequences
developerdoesn't get the shortcut — and that's intentional, not a gap. Still needs amaintainer/ownerto activate execution afterward, exactly as today. Nobody loses capability: whoever only accepted the handoff continues accepting it.- Zero backend contract change.
ExecutionController#activatecontinues withRequireRole('maintainer')unchanged; this is anapps/web-only decision. - One fewer click means an execution session reaching the correct
originSessionIdfaster. RN-135 (the chat session closes when execution takes off via this path) now happens, formaintainer/owner, at the exact moment of acceptance — without the window between the two clicks where the origin session stayedactivewaiting for the second one. - The "Activate execution" button was not removed from the card. It
would be redundant for whoever has the role (the merge already did the
work) and have no effect for whoever doesn't (it would still 403), but
removing it conditionally by role would open up one more state surface to
test with no clear benefit — the whole card already disappears as soon as
the handoff is no longer
offered(accepted), so the window in which the button is "left over" visible and inert is, in practice, the time between the click and the invalidation of the handoffs query — not a functional gap.