ADR 0185 — "I'm ready — the need is validated": one click closes readiness, the necessidade-validada gate and the PO's acceptance
Status
Accepted. 2026-09-30 (AT-311 and AT-312, story HS-079, epic EP-031, round 32). Owner decision, with the two questions of the AT-309 survey answered YES:
"Can a single click 'I'm ready — the need is validated' close both gates, even with the product_brief produced after the click?"
"Can the PO's acceptance be implicit in 'I'm ready'?"
Revises the design of ADR 0095 (the
necessidade-validada gate closes with a SEPARATE click) and
RN-406. It does not edit 0095: it records what
changes and what of it still holds.
Context
Measured on origin/dev (94e5dc721d), from the AT-309 survey:
- "Validate need" locked nothing. The button was enabled only with
artifact.product_briefin the session (hasProductBrief,session-readiness.ts), came AFTER "I'm ready to produce", recorded onlynecessity.validated(ValidateNecessityUseCase), and the gate iswarnindocs/gates.yml— RN-406 itself says nothing in the product CHECKS the passage. - Accepting the PO was one more click right after "I'm ready". The
Criativo→PO handoff is born inside the turn that "I'm ready" triggers
(
CriativoServer.executar_confirm_readiness/1→EngineApiClient.create_handoff→POST /internal/sessions/:id/handoffs), and the acceptance card calledAcceptHandoffUseCase, which records the transition toaccepted,handoff.accepted(actor the person),agent.activated(actor the person) and, by RN-635, thehandoff.supersededof the other offers to the PO in the project. - Three clicks — ready, validate, accept — to express one intent: "what the Criativo gathered is right, go ahead".
0095 explicitly refused merging the validation into confirm_readiness, with
an argument that is still right: confirm_readiness alone is the structural
FLOOR ("≥1 captured rule", RN-142), not a judgment of MERIT, and merging the two
"would keep the shallow floor disguised as a new gate". What answers that
objection here is the LABEL: the click stops being "I'm ready to produce" and
becomes "I'm ready — the need is validated". The person declares both things
with the words of both; the structural floor is still checked by the engine, and
merit is still declared by a human.
Decision
1. One POST, two events (RN-657)
POST .../sessions/:id/readiness (ConfirmReadinessUseCase) records
readiness.confirmed, calls the engine and, only if the engine accepted the
turn, records necessity.validated with the same person as actor,
productBriefId: null, via: 'readiness.confirmed' and the readinessEventId.
A null productBriefId is the answer to the second half of the owner's
question: the brief is born from the turn the click triggers, and the event
points to the CLICK, not to a brief that does not exist yet.
The engine's refusal (409 turn in progress, 422 no rule) surfaces before the
second event: without an accepted turn there is no brief on the way, and the
need is not validated. readiness.confirmed is still recorded BEFORE the
command, as always (ADR 0163).
The gate stays warn. Nothing started checking it, and promoting it remains a
product decision with its own ADR, as 0095 said.
2. The PO's acceptance is implicit, with the SAME events (RN-658)
The readiness.confirmed of the new click carries the mark
{ necessidadeValidada: true, aceiteImplicitoDoPo: true }. When the engine
creates the Criativo→PO offer through the internal route, the api asks
(decidirAceiteImplicitoDoPo, a pure rule) whether it is the offer that click
asked for — criativo → po, offered, from this session, carrying the
product_brief born AFTER the most recent marked readiness.confirmed — and,
if so, accepts it through AcceptHandoffUseCase, the SAME use case as the card,
on behalf of whoever clicked.
The auditable trail is the card's, with no new event: handoff.accepted and
agent.activated with the person as actor, and the mark
implicito: { via: 'readiness.confirmed', readinessEventId } in the payload of
both. Of the two options the owner left (a new event, or the same ones with a
human actor and a mark), the second was chosen because every reader today —
canActivateAgent, the handoff list, the graph projection, the screen —
already understands handoff.accepted; a new event would require teaching each
one, and a forgotten one would read the PO as never accepted. No event is
edited.
Acceptance happens on the internal route and not on the click's POST because
the offer does not exist at the moment of the click. The acceptance failure does
not surface to the engine (the Criativo would narrate "I couldn't offer the
handoff", false): it becomes a durable agent.error, actor
system/aceite-implicito, with origin. The 400 "is not offered" is not a
failure — another tab decided the offer in the interval, and that is already in
the log.
3. The engine does not change
Measured: the Criativo expects nothing from the acceptance (the internal route's
response is only matched with {:ok, _}), and the PO is activated by the same
usual startAgent, whose kickoff is a GenServer.cast. The only difference
is the MOMENT: the PO comes up inside the offer creation call, still in the
Criativo's turn, and not after a human click.
4. The screen
The button is called "Estou pronto — a necessidade está validada" (en: "I'm
ready — the need is validated"), the enabled title says what the click closes
and that the PO comes in on its own, and the success notice repeats that. The
"Confirm validated need" button went away, with handleValidateNecessity and
the client's validateNecessity. The click also makes the PO the composer's
recipient — it is the gesture of calling it, the same as accepting through the
card (RN-631).
Consequences
For
- From three clicks to one, without touching any ceiling: accepting the PO does not provision a repository (only Arquiteto and Dev Lead, RN-582), does not merge nor push.
- Validation is still done by a PERSON, never by the Criativo — the anti-pattern 0095 existed to avoid is still avoided.
- The acceptance trail is the same as the card's, readable by every existing consumer, and the mark says which click it came from.
Against, declared
necessity.validatedstops pointing to the brief it validated: the link is throughreadinessEventId, and the brief is theartifact.product_brieffollowing that click. Whoever needs the exact pair reads the log in order.- The validation is declared BEFORE the person reads the summary. That is what
the owner decided; the gate is
warnand measures the declaration, not the reading. POST .../agents/criativo/validate-necessitystays in the api, with no consumer on screen: it is the path for a session whose "I'm ready" predates this ADR (there the acceptance card also stays, because the old event does not have the mark). Removing it is an API contract break and is left for when no old session uses it.- The PO comes up without a human click at the moment it comes up. The human decided earlier, at the click, and is the recorded actor.
Alternatives considered
A new event (handoff.accepted_implicitly). Refused: see point 2 — every
reader of handoff.accepted would have to learn the second type.
Accepting on the click's POST, pre-creating the offer. Refused: the offer
carries the product_brief as artifact and is born from the Criativo's turn;
creating it earlier would require changing the engine and offering a handoff
without the artifact that justifies it.
Letting the engine call the human acceptance route. Refused: the human route requires an authenticated user, and an agent activating an agent is exactly what the internal route's description refuses. Here the api decides, through the mark the PERSON recorded.
References
- ADR 0095 — the design this revises
- ADR 0163 — the click responds on accept
- ADR 0182 —
superseded, preserved - RN-406, RN-657, RN-658, RN-633, RN-635
apps/api/src/domain/sessions/estou-pronto.tsapps/api/src/application/use-cases/agents/confirm-readiness.use-case.tsapps/api/src/application/use-cases/agents/aceite-implicito-do-po.use-case.ts