ADR 0088 — Staff: code ready, dormant for automatic trigger
- Status: Accepted
- Date: 2026-08-16
- Context:
docs/fluxo.yml, blockid: staff(camada_decisao_tecnica),status: plannedsince ADR 0085 - Direct precedent: ADR 0053 (Dev Lead — solo conversational agent, no area), ADR 0068 (artifact without table, versioned in the event log)
Context
docs/fluxo.yml has declared the Staff/Principal Engineer since ADR 0085
as status: planned: "contract ready, activation decided, awaiting
trigger." The trigger envisioned for it is the Anamnese noticing a
RECURRING systemic problem — but the Anamnese is paused
(ANAMNESE_ENABLED=false, product decision from 2026-08-10) and won't
propose any handoff while that lasts.
The product owner decided to get ahead on the CODE even knowing the automatic trigger won't fire: the Staff becomes ready and can be triggered MANUALLY (any agent or the user can address a handoff to it today, via the generic internal route), and only the automatic trigger remains pending. This is NOT a half-implementation — it's the same distinction ADR 0081 already made for the container lifecycle (table ready, orchestrator not yet): the missing piece is declared, not hidden.
Decision
The Staff is born as the FIFTH solo conversational agent (alongside Criativo, PO, Arquiteto, and Dev Lead), mirroring the Arquiteto's mold exactly — per-session GenServer, event-log rehydration, its own bounded tool-use loop (cap 14, same tier as Arquiteto/Dev Lead: reasoning, not light conversation) — with two deliberate differences.
1. No kickoff/1
All the other conversational leads synthesize an opening instruction from
the session's event log on a FRESH start (kickoff_instruction/1 in each
one) — there's always a prior artifact (product_brief, module_map,
backlog) to summarize. The Staff has no such source: the systemic problem
that would bring it up comes from OUTSIDE (the Anamnese, when it exists,
or whoever creates the handoff manually today), not from the session's
own event log. With no kickoff, Engine.Agents.StaffSupervisor.start_agent/2
brings the process up (rehydrating the history) and it stays idle until
the first user_message — which is how whoever addressed the handoff
explains the problem.
apps/engine/lib/engine_web/controllers/agent_command_controller.ex
reflects this: the "staff" start/2 clause NEVER calls
StaffServer.kickoff/1 (the function doesn't even exist), unlike
po/arquiteto/dev-lead/infra, which call it conditionally when
origin == :started.
2. Activation: the GENERIC path, without USER_STARTED_AGENTS
USER_STARTED_AGENTS (apps/api/src/domain/sessions/agent-activation.ts)
is the Criativo's exception — it starts WITHOUT a handoff, by direct user
command. Investigation confirmed the Staff does NOT need to enter that
list: canActivateAgent already activates any agent with an accepted
handoff addressed to it, the same path already valid for
dev-lead/arquiteto/infra today. No domain change was necessary in
the api for this:
assertHandoffTargetAllowed(apps/api/src/domain/agents/agent-areas.ts) only REJECTS a handoff addressed to an area SUBAGENT (ADR 0038); the Staff has no area — it passes through freely, like Criativo/PO/Arquiteto/Dev Lead.ActivateAgentUseCase/ApiToEngineClient.startAgentare generic overagent: string— no enum to extend.
The consequence is honest: "triggerable manually" means the domain
MECHANICS allow the handoff and the activation, not that there's today a
dedicated screen for a human to choose "address a handoff to the Staff"
without going through another agent's code or a direct call to the
internal route (POST .../internal/sessions/:sessionId/handoffs) — the
generic "manual handoff to an agent of choice" UI remains in the backlog
(docs/explanation/backlog.md), as it already was before this change.
3. propose_rfc — the only tool, with no proposed_action
Engine.Agents.StaffTools.propose_rfc writes artifact.rfc_staff
(problem, options with trade-offs, recommendation, throwaway PoC) and
returns the handoff to the Arquiteto in the SAME tool call — no human
confirmation in between, the same pattern CriativoServer uses when
emitting the product_brief and offering the handoff to the PO in the same
response.
Two real precedents decided the mechanism, and because of that NO new use case was born in the api:
Engine.Harness.Tools.EmitInsight(the Arquiteto'semit_insight) is the pattern for an artifact WITHOUT derivation: the whole payload comes from the tool call, and the event is written directly viaEngineApiClient.append_event_returning/3— no table, no dedicated use case.artifact.c4_diagram(ADR 0068) is the CONTRAST that confirms the choice: it has its own use case in the api because the Container level is DERIVED from the currentmodule_map— only the api has that data. The RFC doesn't derive anything from that side.
propose_rfc is also NOT a proposed_action: recording an architecture
document isn't an external effect (it's not git, terminal, or agent
spend) — the real decision (adopt, adapt, reject) belongs to the
Arquiteto, in the handoff the tool already returns. Same reasoning as
emit_insight.
Consequences
For
- The Staff exists end to end in the engine (server, tool, supervisor,
routing) and is testable today, without waiting for the Anamnese to
come out of refinement — when it comes back, all that's missing is the
Anamnese's CODE calling
create_handoff(..., "staff", ...); the rest is already ready. - No domain change was needed in the api for activation — the
investigation proved the generic path already sufficed, avoiding
opening a new exception (
USER_STARTED_AGENTS) that would need to be revoked later. docs/fluxo.ymlstops being the only source that knows the Staff exists: the code matches what the record already declared, with the real gap (automatic trigger) precise instead of generic ("awaiting trigger").
Against, declared
- The team panel roster (
apps/web/src/lib/agent-status.ts) and the dashboard card (ProjectCardSummary.roster, viaapps/api/.../projects-summary.repository.ts) show the Staff when active —staffActivewas added to both paths, on the same criterion asinfraActive, to avoid repeating the divergence theRosterFactscomment already warned about ("two rules would diverge at the first new agent"). - The Session screen (
SessionPage.tsx) was NOT touched.staffdoesn't enterAGENTES_DE_CHAT— the same pattern already accepted forinfra(a REAL, ACTIVE agent, also outside that list: the file's own comment documents that the handoff to Infra "is never accepted HERE"). This isn't a NEW gap for the Staff: it's the pattern the product already had for a lead without conversation driven by the session composer. Real declared difference: Infra haskickoff/1(it runs on its own once accepted); the Staff does not — so, without a chat surface sending the firstuser_messageto it, today's end-to-end usage path is the internal route (POST .../agent/message,agent: "staff"), not the screen. Fixing this means touchingSessionPage.tsx, the product's most contested file, and wasn't part of this decision — anticipating the code, not the chat UI. - No entry in
Engine.Harness.Agents.identity/1(apps/engine/lib/engine/harness/agents.ex) — the same statedev-leadis in today, which also falls back to the generic fallback ("You are the staff agent."). This isn't a NEW gap: it's the real state of an active precedent, not fixed in passing.
Alternatives considered
Wait for the Anamnese to come out of refinement before building the Staff. Rejected — explicit decision of the product owner: anticipating the code eliminates the "second PR" on the day the Anamnese reconnects, and the value of MANUAL triggering exists independent of the automatic trigger.
Add staff to USER_STARTED_AGENTS. Rejected — the investigation
showed it isn't necessary: the standard canActivateAgent path already
activates on an accepted handoff, and that list exists for the OPPOSITE
exception (an agent that starts WITHOUT a handoff). Adding it here would
invent a rule the domain doesn't call for.
Give the Staff a kickoff/1 with a generic instruction ("investigate
systemic problems"). Rejected — without a real artifact to summarize
(which only the Anamnese will produce), the kickoff would be an empty
invitation; the session stays idle until the first message, and it's
honest that it does.
References
docs/fluxo.yml—id: staffblockdocs/business-rules.md— RN-305/RN-306- ADR 0053 — the mold for a solo, area-less conversational agent
- ADR 0068 — the contrast that decides "no dedicated use case" for the RFC
- ADR 0081 — the precedent of "piece ready, real consumer not yet" declared without softening it
apps/engine/lib/engine/agents/staff_server.ex,staff_tools.ex,staff_supervisor.exapps/engine/lib/engine_web/controllers/agent_command_controller.exapps/api/src/domain/sessions/agent-activation.ts,apps/api/src/domain/agents/agent-areas.tsapps/web/src/lib/agents.ts,apps/web/src/lib/agent-status.ts