Baixar a especificação OpenAPI:Baixar
A API do Brabo. Todo endpoint devolve JSON, exceto os quatro explicitamente
documentados com outro content-type (os dois de SSE, o de métricas,
e o callback OAuth, que redireciona).
O access token é um JWT EdDSA de 15 minutos, apresentado em
Authorization: Bearer <token>. A chave pública mora em
GET /.well-known/jwks.json.
O refresh não é JWT: é um token opaco que mora num cookie
httpOnly (brabo_refresh, Path=/auth, SameSite=Strict) e
nunca aparece em corpo de resposta nenhum. POST /auth/refresh e
POST /auth/logout leem o cookie e exigem que o cabeçalho X-CSRF-Token
bata com o cookie brabo_csrf.
A rotação é obrigatória. Cada refresh consome o token apresentado e emite outro. Reapresentar um já consumido é assinatura de roubo e revoga a família INTEIRA — inclusive a sessão legítima. Um cliente que dispara dois refreshes concorrentes derruba a si mesmo; use uma única chamada em voo.
As rotas /internal/* são a superfície api ↔ engine e ficam fora do
JWT: exigem o cabeçalho X-Brabo-Service-Token com o segredo
compartilhado. Token de usuário não abre nenhuma delas.
Formato padrão do NestJS: { "statusCode": number, "message": string | string[], "error": string }.
| código | quando |
|---|---|
400 |
corpo inválido — o ValidationPipe roda com whitelist e forbidNonWhitelisted, então campo desconhecido também reprova |
401 |
sem token, token expirado ou credencial inválida |
403 |
autenticado mas sem o papel que o RBAC exige, ou service token errado |
404 |
o recurso não existe ou é invisível para quem chama |
409 |
conflito de estado — transição de sessão inválida, recurso já existe |
429 |
rate limit ou bloqueio progressivo |
As rotas de auth respondem de forma deliberadamente indistinguível entre e-mail inexistente, senha errada e conta bloqueada: mesmo corpo, mesmo status, mesmo tempo. É isso que fecha a enumeração de contas, e não é defeito de diagnóstico.
Janela deslizante no Postgres: 300 requisições por usuário e 600 por IP a cada 60s,
configurável. Rota @Public() e rota de serviço são isentas — o que
protege /auth/* é o bloqueio progressivo, não o rate limit.
E-mail inexistente, senha errada e conta bloqueada devolvem exatamente a mesma resposta 401 — mesmo corpo, mesmo status.
| email required | string |
| senha required | string |
{- "email": "fulano@brabo.dev",
- "senha": "correct horse battery staple"
}{- "accessToken": "string",
- "expiresIn": 900,
- "locale": "pt-BR"
}Sucesso vai para WEB_ORIGIN/ já com os cookies da sessão gravados; falha vai para WEB_ORIGIN/login?oauth_error=1, sem detalhar o motivo.
| provider required | string Valores: "github" "gitlab" |
| code required | string |
| state required | string |
O refresh apresentado é consumido. Reapresentar um token já rotacionado revoga a família INTEIRA e grava um evento de segurança — mesmo quando a causa foi um duplo envio do cliente.
{- "accessToken": "string",
- "expiresIn": 900,
- "locale": "pt-BR"
}Responde 202 tanto para endereço novo quanto para um já cadastrado — a resposta não revela se a conta existe. No segundo caso nada é criado e o dono do endereço recebe um aviso.
| email required | string O e-mail da conta. |
| senha required | string >= 12 characters Senha. Mínimo de 12 caracteres; sem exigência de maiúscula, dígito ou símbolo. |
| nome | string |
{- "email": "fulano@brabo.dev",
- "senha": "correct horse battery staple",
- "nome": "Fulano de Tal"
}{- "message": "If the address is available, we've sent an email."
}Responde 202 tanto para endereço conhecido quanto desconhecido. É também o caminho de quem foi importado do Keycloak e ainda não definiu senha.
| email required | string |
{- "email": "fulano@brabo.dev"
}{- "message": "If the address is available, we've sent an email."
}Revoga TODAS as sessões do usuário. Não emite tokens: quem redefine a senha é mandado ao login, para que comprometer o e-mail não equivalha a tomar a conta num passo só.
| token required | string Token de uso único recebido por e-mail. |
| novaSenha required | string >= 12 characters |
{- "token": "string",
- "novaSenha": "another pretty long passphrase"
}Índice declarativo dos gates do fluxo (ADR 0054). A tela deriva daqui quais etapas existem e quem julga cada uma, em vez de repetir a lista no código — que era como ela envelhecia sem ninguém notar.
{- "version": 1,
- "gates": [
- {
- "id": "qa-verificada",
- "fluxo": "pr",
- "dono": "area-qa",
- "entrada": [
- "pr-aberta"
], - "entregavel": "veredito-qa",
- "aprovacaoHumana": true,
- "severidade": "block"
}
]
}Ciclo de gate parado há mais de 2 h é estacionado em vez de reiniciado sozinho (ADR 0207). Este é o gesto humano que o retoma.
| projectId required | string |
| taskId required | string |
| gate required | string |
{- "ok": true
}Pública porque quem chega é o NAVEGADOR do usuário vindo do provider, sem sessão da api. Não é irrestrita: o state é verificado por HMAC e sem um válido a chamada é recusada. Nunca responde JSON — sempre redireciona, porque um corpo de erro cru numa navegação do navegador seria uma experiência péssima.
| provider required | string Valores: "github" "gitlab" |
| code required | string |
| state required | string |
Base para a pendência declarada do blame (FASE 26b) — a interface que anota o editor linha a linha ainda não existe. truncated avisa quando o arquivo passou do teto de linhas anotadas.
| projectId required | string |
| path required | string Caminho do arquivo a anotar. |
| ref | string Branch, tag ou sha. Ausente usa a branch padrão do repositório. |
{- "ref": "dev",
- "path": "apps/api/src/main.ts",
- "lines": [
- {
- "line": 12,
- "commitSha": "0f3e8181fbd010b10c78db17b90ecb35fb8cc89c",
- "author": "Daniel Souza",
- "authorDate": "2026-08-04T12:00:00.000Z",
- "summary": "fix(api): fix the credential resolver"
}
], - "truncated": false
}Base para o dropdown rico (FASE 26b) — hoje o seletor de ref é um campo de texto simples. ahead/behind são relativos à branch PADRÃO do repositório; null quando o provider não consegue calcular (degradação honesta, não erro). NÃO é listBranches (que o bootstrap do Gitflow usa) — é operação própria, porque enriquecer custa uma chamada a mais ao provider POR BRANCH.
| projectId required | string |
{- "items": [
- {
- "name": "feature/x",
- "commitSha": "0f3e8181fbd010b10c78db17b90ecb35fb8cc89c",
- "protected": false,
- "ahead": 3,
- "behind": 0,
- "pullRequest": {
- "number": 42,
- "state": "open"
}, - "producedBy": {
- "agentId": "dev-pieces",
- "moduleId": "pieces"
}
}
], - "truncated": false
}UTF-8, cortado no teto de bytes com truncated: true quando passa dele. Caminho que escapa da pasta do projeto é recusado com 400 pela checagem central da RN-095 — nunca com 404, que convidaria a sondar.
| projectId required | string |
| path required | string Caminho do arquivo. |
| ref | string Branch, tag ou sha. |
{- "ref": "dev",
- "path": "apps/api/src/main.ts",
- "content": "import { NestFactory } from '@nestjs/core';\n",
- "truncated": false,
- "bytes": 1284
}Base para a lista navegável (FASE 26b) — hoje o diff só é alcançável por um id conhecido (ex.: vindo de Aprovações); esta rota resolve como chegar ao id sem ter de sabê-lo de cor. truncated avisa quando a lista passou do teto por chamada.
| projectId required | string |
| state | string Valores: "open" "merged" "closed" Ausente lista todos os estados. |
{- "items": [
- {
- "id": "2401938475",
- "number": 42,
- "title": "fix(api): the agents area is born with the project",
- "author": "daneiel",
- "state": "open",
- "sourceBranch": "feature/x",
- "targetBranch": "dev",
- "updatedAt": "2026-08-04T12:00:00.000Z"
}
], - "truncated": false
}patch nulo distingue o caso em que o provider não entregou o texto (binário, ou patch grande demais) do caso em que o texto voltou vazio. Juntar os dois faria a tela dizer que um binário alterado não mudou. O campo truncated avisa quando a lista de arquivos passou do teto.
| projectId required | string |
| pullRequestId required | string |
{- "pullRequestId": "pr-1",
- "files": [
- {
- "path": "apps/api/src/main.ts",
- "previousPath": null,
- "status": "modified",
- "additions": 12,
- "deletions": 3,
- "patch": "@@ -1,3 +1,4 @@\n+import x\n"
}
], - "truncated": false
}A busca NÃO é operação do contrato de git: é composta sobre a árvore e o conteúdo, e por isso é a única leitura cujo custo cresce com o tamanho do repositório. Três orçamentos a limitam — diretórios percorridos, arquivos abertos e acertos devolvidos — e truncated diz que ela parou. filesScanned mostra o custo que ela teve.
| projectId required | string |
| q required | string Termo, de 2 a 200 caracteres. |
| ref | string Branch, tag ou sha. |
| path | string Subárvore onde buscar. Estreitar isto é o que evita o corte. |
{- "ref": "dev",
- "path": "",
- "query": "NestFactory",
- "matches": [
- {
- "path": "apps/api/src/main.ts",
- "line": 12,
- "text": " const app = await NestFactory.create(AppModule);"
}
], - "filesScanned": 87,
- "truncated": true
}Não recursiva, por desenho do contrato: a aba navega sob demanda, e pedir a árvore inteira de um repositório grande é exatamente o amplificador de tráfego que esta fase proíbe. truncated avisa quando o nível passou do teto de entradas.
| projectId required | string |
| ref | string Branch, tag ou sha. Ausente usa a branch padrão do repositório. |
| path | string Diretório a listar. Ausente ou vazio é a raiz. |
{- "ref": "dev",
- "path": "apps/api",
- "entries": [
- {
- "path": "apps/api/src/main.ts",
- "name": "main.ts",
- "type": "file",
- "size": 1284
}
], - "truncated": false
}Não redireciona: devolve a URL para o cliente decidir quando mandar o navegador. O state vai assinado por HMAC, e é isso que impede o callback de ser forjado.
| projectId required | string |
| provider required | string Valores: "github" "gitlab" |
{
}SÍNCRONA: a resposta só volta depois que o bootstrap inteiro (branches permanentes, proteções, templates) rodou — não há worker nem fila por trás. É IDEMPOTENTE E RETOMÁVEL, então chamá-la de novo depois de uma falha retoma do passo que falhou. Acompanhe o progresso e leia o motivo da falha por GET /projects/:id/git/bootstrap.
| projectId required | string |
| provider required | string Valores: "local" "github" "gitlab" |
| name required | string Nome do repositório no provider. |
| visibility required | string Valores: "public" "private" |
| namespace | string Organização ou grupo. Quando omitido, o repositório é criado na conta pessoal de quem registrou a credencial. |
{- "name": "checkout",
- "visibility": "private",
- "namespace": "acme"
}{- "repository": {
- "id": "01JC4Z0000REPOSITORIO000001",
- "projectId": "01JC4Z0000PROJETO0000000001",
- "provider": "github",
- "externalId": "904837261",
- "defaultBranch": "main",
- "visibility": "private",
- "origin": "created",
- "provisionedBy": "01JC4Z0000USUARIO0000000001",
- "createdAt": "2026-07-23T10:00:00.000Z",
- "updatedAt": "2026-07-23T10:00:00.000Z"
}, - "bootstrap": {
- "step": "create_dev_branch",
- "status": "running"
}
}Valida o acesso com getRepo e produz um PLANO (dry-run) do que o bootstrap faria — branches faltando, proteções faltando, arquivos. NADA é executado no repositório: decida depois por plan/approve ou plan/skip. Readotar o mesmo repositório converge (regenera o plano sem duplicar linha).
| projectId required | string |
| provider required | string Valores: "local" "github" "gitlab" |
| externalId required | string O identificador do repositório NO PROVIDER: |
{- "externalId": "acme/checkout"
}{- "repository": {
- "id": "01JC4Z0000REPOSITORIO000001",
- "projectId": "01JC4Z0000PROJETO0000000001",
- "provider": "github",
- "externalId": "904837261",
- "defaultBranch": "main",
- "visibility": "private",
- "origin": "created",
- "provisionedBy": "01JC4Z0000USUARIO0000000001",
- "createdAt": "2026-07-23T10:00:00.000Z",
- "updatedAt": "2026-07-23T10:00:00.000Z"
}, - "plan": {
- "generatedAt": "2026-08-01T23:45:00.000Z",
- "steps": [
- {
- "step": "create_qa_branch",
- "actionType": "git_branch_create",
- "payload": {
- "branchName": "qa",
- "fromRef": "dev"
}
}
], - "diagnostics": [
- {
- "kind": "extra_branch",
- "detail": {
- "branchName": "develop",
- "protected": false
}
}
]
}, - "alreadyAdopted": false
}Em que passo está, se falhou e onde, e quantas tentativas houve. O detalhe passo a passo vem dos eventos bootstrap.step_* da sessão dedicada.
| projectId required | string |
{- "status": "provisioning",
- "sessionId": "01JC4Z8QK3M7YV2N5T9B0PXHRA",
- "failedStep": null,
- "lastError": null,
- "attempts": 1
}protect_branches falha em repositório privado no plano gratuito, e o assistente avisa disso ANTES. Sem esta saída o único botão era "Tentar de novo", que sempre falha pelo mesmo motivo — e provision_failed faz o painel redirecionar o projeto de volta para a página de provisionamento, deixando-o inalcançável para sempre. Só falha em PROTEGER pode ser reconhecida: é o último passo e o único cuja falha deixa um repositório utilizável. A trava de merge do produto (RN-006) não depende da proteção do provider e continua valendo.
| projectId required | string |
{- "status": "provisioned"
}decision: null com plano presente é o estado em que NADA roda — aguardando aprovação ou adoção como está (RN-045).
| projectId required | string |
{- "plan": {
- "generatedAt": "2026-08-01T23:45:00.000Z",
- "steps": [
- {
- "step": "create_qa_branch",
- "actionType": "git_branch_create",
- "payload": {
- "branchName": "qa",
- "fromRef": "dev"
}
}
], - "diagnostics": [
- {
- "kind": "extra_branch",
- "detail": {
- "branchName": "develop",
- "protected": false
}
}
]
}, - "decision": null,
- "decidedAt": null,
- "decidedBy": null
}A aprovação é tudo ou nada: aprovar passos soltos quebraria a cascata dev←main, qa←dev, rc←qa. O que roda é o plano RE-DERIVADO na hora da execução — igual ou menor que o mostrado, nunca maior.
| projectId required | string |
| planGeneratedAt required | string <date-time> O |
{- "planGeneratedAt": "2026-08-01T23:45:00.000Z"
}{- "repository": {
- "id": "01JC4Z0000REPOSITORIO000001",
- "projectId": "01JC4Z0000PROJETO0000000001",
- "provider": "github",
- "externalId": "904837261",
- "defaultBranch": "main",
- "visibility": "private",
- "origin": "created",
- "provisionedBy": "01JC4Z0000USUARIO0000000001",
- "createdAt": "2026-07-23T10:00:00.000Z",
- "updatedAt": "2026-07-23T10:00:00.000Z"
}, - "bootstrap": {
- "step": "create_dev_branch",
- "status": "running"
}
}Grava a decisão e não toca o repositório. O plano fica guardado como evidência do que deliberadamente não foi aplicado, e o cursor do bootstrap NÃO é mexido — nenhum passo rodou.
| projectId required | string |
| planGeneratedAt required | string <date-time> O |
{- "planGeneratedAt": "2026-08-01T23:45:00.000Z"
}{- "repository": {
- "id": "01JC4Z0000REPOSITORIO000001",
- "projectId": "01JC4Z0000PROJETO0000000001",
- "provider": "github",
- "externalId": "904837261",
- "defaultBranch": "main",
- "visibility": "private",
- "origin": "created",
- "provisionedBy": "01JC4Z0000USUARIO0000000001",
- "createdAt": "2026-07-23T10:00:00.000Z",
- "updatedAt": "2026-07-23T10:00:00.000Z"
}, - "bootstrap": {
- "step": "create_dev_branch",
- "status": "running"
}
}| projectId required | string |
{- "id": "01JC4Z0000REPOSITORIO000001",
- "projectId": "01JC4Z0000PROJETO0000000001",
- "provider": "github",
- "externalId": "904837261",
- "defaultBranch": "main",
- "visibility": "private",
- "origin": "created",
- "provisionedBy": "01JC4Z0000USUARIO0000000001",
- "createdAt": "2026-07-23T10:00:00.000Z",
- "updatedAt": "2026-07-23T10:00:00.000Z"
}Executa um select 1. Revela só se o banco responde; nem topologia nem versão. Pública porque quem chama é o kubelet, que não carrega token.
{- "service": "api",
- "status": "ok",
- "timestamp": "2026-07-27T15:40:00.000Z",
- "details": { }
}NÃO toca o banco, de propósito. Se tocasse, um Postgres lento reiniciaria TODAS as réplicas de uma vez e transformaria degradação em queda total. O pod sai do balanceador pelo /health.
{- "service": "api",
- "status": "ok",
- "timestamp": "2026-07-27T15:40:00.000Z",
- "details": { }
}Não é JSON: é text/plain no formato de exposição. Pública por necessidade (o scraper não carrega token) e restrita por REDE — o Ingress de produção bloqueia este caminho na borda e a NetworkPolicy só libera o namespace de monitoramento.
# HELP oban_queue_depth Oban queue depth # TYPE oban_queue_depth gauge oban_queue_depth{queue="default",state="available"} 0
Proxy de GitHub Releases — platform aceita só linux-x64, linux-arm64, darwin-arm64. Sem autenticação: o binário não é segredo. Os bytes são conferidos contra o checksums.txt da mesma Release antes de qualquer coisa ser respondida (ADR 0149, RN-525); a rota NÃO verifica a assinatura do manifesto — isso é integridade, não procedência, e quem verifica assinatura é o install.sh.
Para o instalador de uma linha, que roda onde nenhum transporte de e-mail está configurado: o .env gerado não traz variável de e-mail, MAIL_TRANSPORT cai em log, e o cadastro normal espera um e-mail que nunca chega. A conta nasce verificada porque quem roda o instalador já provou algo MAIS FORTE que controlar uma caixa de e-mail — controla a máquina, o .env e o daemon do Docker. O cadastro normal não muda. Recusa com 409 quando a instalação tem QUALQUER usuário: a condição é sobre a instalação, não sobre este e-mail, então a restauração de uma migração silencia o passo pelo mesmo teste. A senha nunca é gerada aqui e nunca é guardada em lugar nenhum senão como hash argon2id; o workspace pessoal (RN-410) nasce na mesma transação.
| email required | string O e-mail do owner, digitado no TTY. Normalizado antes de qualquer gravação; é o que o instalador grava no marcador dele, já que e-mail identifica e não é segredo. |
| senha required | string Lida sem eco e confirmada pelo instalador. Usada e descartada — nunca gravada no |
| nome | string Nome de exibição. Omitido: o nome do workspace pessoal cai na parte local do e-mail (RN-410) — nada é inventado. |
{- "email": "voce@exemplo.dev",
- "senha": "uma frase longa e minha",
- "nome": "Fulana de Tal"
}{- "userId": "01JC4Z0000USUARIO0000000001",
- "email": "voce@exemplo.dev",
- "workspaceId": "01JC4Z0000WORKSPACE00000001"
}O registro DESCREVE, não executa: nenhum gate passa a ser aplicado por causa dele, e mudar um campo aqui não muda comportamento nenhum. Cada gate diz onde mora a prova de que passou (evidencia), porque nem toda prova está no event log.
{- "version": 1,
- "gates": [
- {
- "id": "qa-verificada",
- "fluxo": "pr",
- "dono": "area-qa",
- "entrada": [
- "pr-aberta"
], - "entregavel": "veredito-qa",
- "verificacao": "script",
- "severidade": "block",
- "aprovacao_humana": true,
- "status": "active",
- "evidencia": { },
- "backlog": "ADR 0053"
}
]
}Se já existe uma versão com o MESMO hash para o mesmo name, nenhuma versão nova é criada — a existente é devolvida (o acerto de idempotência não muda o status HTTP: sempre 201, por simplicidade — o corpo é idêntico ao que teria sido criado).
| name required | string |
| version required | string |
| body required | string |
| hash required | string |
{- "name": "dev-agent-kickoff",
- "version": "3",
- "body": "You are the dev agent for the {{modulo}} module...",
- "hash": "sha256:9f2c..."
}{- "name": "dev-agent-kickoff",
- "version": "3",
- "body": "You are the dev agent for the {{modulo}} module...",
- "hash": "sha256:9f2c...",
- "active": true
}Uma version específica busca por igualdade; omitida, busca a versão active mais recente. 404 se o template (ou a versão pedida) não existe.
| name required | string |
| version | string Exemplo: version=3 |
{- "name": "dev-agent-kickoff",
- "version": "3",
- "body": "You are the dev agent for the {{modulo}} module...",
- "hash": "sha256:9f2c..."
}Para o instalador de uma linha, logo depois de criar a primeira conta: o agente local precisa de uma credencial, e a única que existia era presa a um PROJETO — numa instalação que ainda não tem nenhum. O par é gerado NA MÁQUINA e só a metade pública chega aqui; grave o id devolvido na JWK privada como kid (RN-475), que é o único vínculo entre o arquivo em disco e a metade pública no servidor. Não há userId no corpo DE PROPÓSITO: o dono é o ÚNICO usuário da instalação, resolvido pela api, então ter o service token nunca significa escolher de quem é a credencial cunhada. Registrar SUBSTITUI: as chaves de máquina ativas do dono são revogadas na mesma transação, então uma máquina reinstalada funciona e mil chaves de máquina não podem existir. Chaves de projeto (ADR 0118) nunca são tocadas.
| name required | string Um nome para um humano reconhecer esta MÁQUINA depois, na lista de chaves de dispositivo de todo projeto que ela atende. Não é único. O instalador sugere o hostname; nada aqui é derivado dele pela api. |
| publicKeyJwk required | string A JWK PÚBLICA Ed25519 (RFC 8037), serializada como JSON. O par é gerado na máquina e a metade privada nunca viaja — JWK trazendo |
{- "name": "servidor-de-casa",
- "publicKeyJwk": "{\"kty\":\"OKP\",\"crv\":\"Ed25519\",\"x\":\"…\"}"
}{- "id": "01JC4Z0000CHAVE000000000001",
- "userId": "01JC4Z0000USUARIO0000000001",
- "name": "servidor-de-casa",
- "createdAt": "2026-09-12T12:00:00.000Z",
- "replacedKeyIds": [ ]
}Nunca falha por causa de um provider: cada um responde por si no relatório, com o motivo de ter sido pulado e a origem da falha quando houve. Provider que não respondeu é PULADO, não marcado indisponível.
{- "porProvider": [
- {
- "provider": "openai",
- "descobertos": 3,
- "reencontrados": 1,
- "indisponibilizados": 0,
- "pulado": "sem_capability",
- "origemDaFalha": "infra",
- "detalhe": "openai responded with status 401"
}
]
}A MESMA árvore épico → história → tarefa da aba Backlog, pelo mesmo caso de uso (três leituras por projeto, nunca N+1). É com ela que o PO vê um épico órfão e uma história sem tarefa em vez de recriar o que já existe (RN-164).
| projectId required | string |
[- {
- "id": "01JC4Z0000EPICO000000000001",
- "projectId": "01JC4Z0000PROJETO0000000001",
- "sessionId": "01JC4Z8QK3M7YV2N5T9B0PXHRA",
- "title": "Shopping cart",
- "description": "Everything the buyer does before paying.",
- "createdAt": "2026-07-25T08:30:00.000Z",
- "updatedAt": "2026-07-25T08:30:00.000Z",
- "stories": [
- {
- "id": "01JC4Z0000HISTORIA000000001",
- "epicId": "01JC4Z0000EPICO000000000001",
- "projectId": "01JC4Z0000PROJETO0000000001",
- "sessionId": "01JC4Z8QK3M7YV2N5T9B0PXHRA",
- "title": "Add item to cart",
- "description": "As a buyer, I want to gather items before paying.",
- "rf": [
- "The cart accepts up to 50 items"
], - "rnf": [
- "The response stays under 200 ms at p95"
], - "businessRuleIds": [
- "RN-014"
], - "dod": [
- "Unit tests green"
], - "dor": [
- "Module defined"
], - "moduleIds": [
- "api"
], - "status": "ready",
- "proposedReady": true,
- "returnedReason": "DoD too generic — spell out the acceptance criteria.",
- "returnedAt": "2026-08-02T14:00:00.000Z",
- "archivedAt": null,
- "archivedReason": null,
- "createdAt": "2026-07-25T09:00:00.000Z",
- "updatedAt": "2026-07-25T09:40:00.000Z",
- "tasks": [
- {
- "id": "01JC4Z0000TAREFA00000000001",
- "storyId": "01JC4Z0000HISTORIA000000001",
- "title": "POST /cart/items endpoint",
- "description": "Accepts SKU and quantity; validates stock.",
- "status": "in_progress",
- "assignedTo": "dev-api",
- "blocked": false,
- "blockedReason": null,
- "blockedOrigin": null,
- "gateStatus": null,
- "gateCorrectionCount": 0,
- "module": "api",
- "createdAt": "2026-07-25T10:00:00.000Z",
- "updatedAt": "2026-07-25T12:30:00.000Z"
}
]
}
]
}
]Todo artifact.business_rule das sessões do projeto — não só o da sessão corrente, que era o teto do contexto de kickoff do PO — com a description inteira e quais histórias já citam cada regra. uncoveredCount é a pendência: regra que nenhuma história cobre (RN-164). Projeto sem regra nenhuma responde 200 com lista vazia: "nenhuma regra capturada ainda" não é erro.
| projectId required | string |
{- "rules": [
- {
- "id": "evt_01jc4z0000regra0000000001",
- "title": "Cart accepts at most 50 items",
- "description": "Above that, the addition is refused with 409.",
- "coveredByStoryIds": [
- "01JC4Z0000HISTORIA000000001"
], - "covered": true
}
], - "uncoveredCount": 2
}Chamada só pelo engine, quando Engine.Actions.TerminalExecutor decidiu que o comando cabe dentro do container real (execution_mode: container com uma linha running REGISTRADA). Faz proxy para ContainerBrokerPort.exec (ADR 0130/0134) — a última das cinco operações do broker a ganhar chamador real. sucesso: false na resposta — nunca um erro lançado — é a forma NORMAL de broker que recusou ou é inalcançável: o container pode ter morrido ou sido removido por fora entre a linha running registrada e esta chamada (RN-486, registrado e observado nunca se fundem), e isso é falha do comando, não erro de transporte.
| projectId required | string |
| comando required | string O comando de shell, interpretado por |
| cwd | string Diretório de trabalho DENTRO do container. Já traduzido do caminho do host pelo engine (RN-492) — este DTO não repete essa tradução. Omitido roda em |
| timeoutMs | number Timeout em milissegundos. Omitido usa o padrão do broker. |
object AT-612: run in the task QA instance instead of the work container. Set by the engine from the gate context, never by the model. |
{- "comando": "npm test",
- "cwd": "/work/.worktrees/dev-api",
- "timeoutMs": 300000,
- "alvo": {
- "qa": true,
- "taskId": "e6e9d88a-9b63-468a-aec3-b7a11de27af8"
}
}{- "sucesso": true,
- "exitCode": 0,
- "output": "ok\n",
- "timedOut": false,
- "motivo": "o broker de container não respondeu em http://broker:8090: timeout"
}Identidade do projeto, modo de execução e a decisão de imagem vigente do Arquiteto. O broker revalida tudo antes de entregar qualquer coisa ao daemon — ler daqui não é o mesmo que confiar, e a recusa nomeia o campo. Nenhum caminho ABSOLUTO é devolvido: a origem do bind é resolvida pelo daemon contra o sistema de arquivos do HOST, e um caminho de dentro do container da api montaria em silêncio uma pasta vazia. O que viaja é localizacao (RN-503) — qual das duas raízes do broker usar, mais o segmento relativo que a raiz não cobre.
| projectId required | string |
| papel | string Valor: "qa" |
| task | string |
{- "projectId": "f52be111-0000-4000-8000-000000000000",
- "projectSlug": "exp002",
- "workspaceId": "aaaaaaaa-0000-4000-8000-000000000000",
- "workspaceDirName": "exp002-f52be111",
- "executionMode": "container",
- "localizacao": {
- "tipo": "gerenciada",
- "segmento": "loja",
- "motivo": "o projeto está no modo \"runner\"…"
}, - "usuarioDaPasta": {
- "uid": 1000,
- "gid": 1000
}, - "imagem": {
- "image": "node:22-bookworm-slim",
- "network": "none",
- "resources": {
- "cpus": 2,
- "memoryMb": 4096,
- "pidsLimit": 512
}
}, - "imagemVersao": 3,
- "comandoDoApp": "string",
- "ports": [
- {
- "host": 8080,
- "container": 3000,
- "protocol": "tcp"
}
], - "dns": [
- "string"
], - "extraHosts": [
- {
- "name": "api.local",
- "ip": "10.0.0.5"
}
], - "qa": {
- "chave": "a1b2c3d4",
- "services": [
- { }
]
}
}Devolve a origem limpa (sem credencial embutida) e, para provider remoto, o token do DONO do workspace (RN-058) decifrado na hora. Quem consome injeta o token por invocação e NUNCA o grava em arquivo — .git/config fica dentro da pasta onde a RN-075 concede ao dev agent leitura auto-aprovada.
| projectId required | string |
{- "kind": "remote",
- "defaultBranch": "main",
- "token": "string",
- "username": "x-access-token"
}Chamada só pelo engine, depois que o runner empurra mirror_sync_result pelo canal — o MESMO caminho do workspace_confirm (runner → canal → engine → api), nunca um segundo mecanismo: o engine relata, não grava a tabela. Isto é TELEMETRIA, não evento de domínio: nunca session_events (rodada de espelho não tem sessão e session_id é NOT NULL, o mesmo raciocínio que fez de rag_searches uma tabela) e nunca proposed_action (a escrita do espelho é configuração que o usuário declarou, não agente pedindo para agir). ok é o que decide o desfecho — nunca a presença de uma contagem ou de uma mensagem: rodada que copiou 0 arquivos é normal e não pode ser indistinguível de uma que nunca rodou. Gravar nunca derruba o que mede: a cópia já está feita quando isto é chamado, e o engine só registra em log uma recusa aqui. Projeto cujo destino foi limpo nesse meio-tempo AINDA grava — a rodada aconteceu, e o erro dela costuma ser o que explica o que deu errado.
| projectId required | string |
| ok required | boolean O desfecho REAL da cópia, relatado pelo agente local depois de terminar — nunca um "ok" otimista antes de a rodada acabar. |
| destination | string O destino que a rodada de fato resolveu (sucesso) ou tentou usar (falha), na máquina do USUÁRIO. CONGELADO na linha: mostrar o destino atual do projeto ao lado da cópia de ontem seria a tela afirmando sobre uma pasta que aquela rodada nunca tocou. Omitido quando a falha é sobre o próprio destino. |
| filesCopied | number Arquivos regulares copiados nesta rodada. |
| filesSkipped | number Entradas que o git listou e não são arquivos regulares — um repositório aninhado (o worktree de um dev agent é um), um symlink, ou um arquivo que sumiu entre a listagem e a cópia. |
| filesRefused | number Alvos que a guarda por arquivo recusou (um symlink escapando do destino). Contados, nunca engolidos. |
| error | string A mensagem de falha NOMEADA. Guardada cortada quando muito longa, dizendo que foi cortada. Nunca apaga a última sincronização bem-sucedida — qual linha é a CORRENTE se decide comparando os dois carimbos. |
{- "ok": true,
- "destination": "/home/you/mirrors/store",
- "filesCopied": 412,
- "filesSkipped": 3,
- "filesRefused": 0,
- "error": "o espelho não conseguiu listar o trabalho com o git: not a git repository"
}{- "recorded": true,
- "status": "synced"
}Uma entrada por módulo do module_map VIGENTE: o que ele consome (dependeDe, o dependsOn do mapa) e o que expõe (expoe, do artifact.module_contracts vigente do Arquiteto, ou null quando nenhum foi declarado). É o que um dev agent lê em vez de abrir o worktree de outro módulo (ADR 0200). Projeto sem contrato responde 200 com status: sem_contratos.
| projectId required | string |
{- "status": "sem_contratos",
- "version": 2,
- "modulos": [
- {
- "modulo": "game-session",
- "dependeDe": [
- "board-engine",
- "scoring"
], - "expoe": [
- {
- "tipo": "funcao",
- "assinatura": "placePiece(board: Board, piece: Piece, pos: Pos): Board",
- "descricao": "Returns a new board; never mutates the one passed in."
}
]
}
], - "contratosForaDoMapa": [ ]
}O MESMO relatório do script analise:funil (ADR 0089) — funil sessão → commit → PR → merge, lead time real e frequência de deploy real — pelas mesmas funções puras e a mesma consulta (apps/api/src/application/services/funil-metrics.ts), para que os dois nunca divirjam sobre o mesmo fato. Fecha docs/fluxo.yml (papel po, entrada metricas-de-produto, antes status: lacuna) (RN-407).
| projectId required | string |
{- "project": {
- "id": "01JC4Z0000PROJETO0000000001",
- "name": "exp003"
}, - "totalActionsConsidered": 18,
- "funnel": {
- "etapas": [
- {
- "etapa": "sessão produziu commit",
- "sessoes": 3,
- "taxaDaEtapaAnterior": 0.67
}
], - "sessoesComCommit": [
- "string"
], - "sessoesComPr": [
- "string"
], - "sessoesComMerge": [
- "string"
]
}, - "leadTimes": {
- "perSession": [
- {
- "sessionId": "01JC4Z0000SESSAO000000000001",
- "primeiroCommitEm": "2026-08-01T10:00:00.000Z",
- "primeiroMergeEm": "2026-08-01T12:00:00.000Z",
- "leadTimeMs": 7200000
}
], - "averageMs": 7200000
}, - "deploymentFrequency": [
- {
- "dia": "2026-08-01",
- "merges": 2
}
]
}Automatic, no proposal: it only reaches what carries the QA label (the broker guarantees it). Idempotent.
| projectId required | string |
| taskId required | string |
{- "sucesso": true,
- "status": "provisioning",
- "chave": "a1b2c3d4",
- "motivo": "string"
}Records the row, then asks the broker. sucesso: false is the NORMAL shape of a refused broker. At most ONE active instance per project: another task active -> 409 instancia_de_qa_ocupada. runner projects -> 409 qa_nao_suporta_runner.
| projectId required | string |
| taskId required | string |
{- "sucesso": true,
- "status": "provisioning",
- "chave": "a1b2c3d4",
- "motivo": "string"
}Chamada só pelo engine, depois que um runner conecta e manda workspace_confirm pelo canal. O runner é a FONTE DE VERDADE do caminho — a api sobrescreve workspacePath com o que ele relatou, depois de revalidá-lo lexicamente (raiz de sistema/sobreposição com o Brabo seguem proibidas mesmo vindo do runner). Idempotente: reconectar com o MESMO caminho não grava nada de novo.
| projectId required | string |
| path required | string Caminho absoluto confirmado pelo runner NO HOST — a fonte de verdade (RN-423). Revalidado LEXICAMENTE aqui antes de gravar; inválido é 400, nunca gravado. |
| sessionId | string <uuid> A sessão mais recente do projeto ( |
| actorId | string <uuid> Quem pediu o ticket do runner (dono do socket que confirmou) — vira o ator do evento |
{- "path": "/home/you/projects/store",
- "sessionId": "01JC4Z0000SESSAO00000000001",
- "actorId": "01JC4Z0000USUARIO0000000001"
}{- "verified": true,
- "workspacePath": "/home/you/projects/store",
- "changed": true
}The engine sends the task ids it still has in flight; every other active or failed instance is removed. Shared services and the QA network have no broker operation yet (declared gap).
| taskIdsEmVoo required | Lista de strings Task ids that still have a gate in flight; every other active instance is removed. |
| projectId | string <uuid> Limit the sweep to one project. |
{- "taskIdsEmVoo": [
- "string"
], - "projectId": "5a8591dd-4039-49df-9202-96385ba3eff8"
}{- "removidas": [
- "string"
], - "falhas": [
- {
- "taskId": "string",
- "motivo": "string"
}
]
}Mesmo caso de uso da rota humana — nenhum segundo caminho de julgamento. searchId/chunkId desconhecido é um 400 que o engine transforma em tool-result de erro para o modelo corrigir (RN-061), nunca um crash.
| projectId required | string <uuid> |
| searchId required | string <uuid> |
| chunkId required | string <uuid> |
| verdict required | string Valores: "util" "irrelevante" |
| agent required | string Slug do agente — quem vota. |
{- "projectId": "5a8591dd-4039-49df-9202-96385ba3eff8",
- "searchId": "9b1a67f9-9477-48e5-8a6c-11b70245e1d9",
- "chunkId": "90e1f3df-a8a1-438e-8dd5-10e45398d051",
- "verdict": "util",
- "agent": "qa"
}{- "searchId": "9b1a67f9-9477-48e5-8a6c-11b70245e1d9",
- "chunkId": "90e1f3df-a8a1-438e-8dd5-10e45398d051",
- "verdict": "util",
- "rank": 0
}Mesmo motor de POST /projects/:projectId/rag/search (rota humana), projetado no formato que a ferramenta rag_search do engine espera.
| projectId required | string <uuid> |
| query required | string |
| topK | number |
| sessionId | string <uuid> A sessão em que o agente está rodando. Ausente quer dizer que quem chamou não tinha sessão — a linha de telemetria é gravada assim mesmo (RN-479), só a narração |
| agent | string Slug do agente, gravado como ator da telemetria ( |
{- "projectId": "5a8591dd-4039-49df-9202-96385ba3eff8",
- "query": "how the parallelism cap is decided",
- "topK": 5,
- "sessionId": "f6567dd8-e069-418e-8893-7d22fcf12459",
- "agent": "dev-lead"
}{- "searchId": "9b1a67f9-9477-48e5-8a6c-11b70245e1d9",
- "hits": [
- {
- "chunkId": "90e1f3df-a8a1-438e-8dd5-10e45398d051",
- "path": "docs/adr/0080-chat-rag-pipeline-indexacao.md",
- "chunk": "string",
- "score": 0,
- "excerpt": "string"
}
], - "degraded": true
}A ÚNICA porta pela qual um agente toca git, terminal ou gasto. permissions.json decide na criação, e deny vence qualquer autonomia concedida ao agente.
| sessionId required | string |
| projectId required | string <uuid> |
| actionType required | string Valores: "terminal" "git_commit" "git_push" "pr_open" "spend" "git_repo_create" "git_branch_create" "git_branch_protect" "write_file" "open_adr_pr" "git_merge" "open_infra_pr" "instruction_patch" "parallelize" "raise_max_parallel" "propose_execution_plan" "assess_implementability" "container_start" "container_stop" "container_remove" "container_recreate" "container_start_via_runner" "qa_environment_start" |
required | object Sempre um agente nesta rota. |
required | object |
{- "projectId": "01JC4Z0000PROJETO0000000001",
- "actionType": "terminal",
- "actor": {
- "kind": "agent",
- "id": "dev-api"
}, - "payload": {
- "command": "pnpm test"
}
}{- "id": "01JC4Z8QK3M7YV2N5T9B0PXHRC",
- "projectId": "01JC4Z0000PROJETO0000000001",
- "sessionId": "01JC4Z8QK3M7YV2N5T9B0PXHRA",
- "seq": 7,
- "actionType": "terminal",
- "payload": {
- "command": "pnpm test"
}, - "status": "pending",
- "resolvedPolicy": "require_approval",
- "actor": {
- "kind": "agent",
- "id": "dev-api"
}, - "decidedBy": null,
- "decidedAt": null,
- "rejectionReason": null,
- "executionResult": null,
- "createdAt": "2026-07-27T14:33:10.900Z",
- "updatedAt": "2026-07-27T14:33:10.900Z"
}Tudo numa chamada só, espelhando o contexto do Psicólogo. Os membros já vêm SEM quem optou por não ser perfilado, e as decisões do usuário dentro da janela vêm por aqui porque não estão no event log. O engine lê os eventos em si direto do Postgres — carregá-los por HTTP seria mais caro sem ser mais correto.
| projectId required | string |
{- "competencyCatalog": [
- "TypeScript",
- "Elixir",
- "Kubernetes"
], - "members": [
- {
- "userId": "01JC4Z0000USUARIO0000000002",
- "name": "Senior Dev",
- "email": "dev@brabo.dev",
- "role": "developer"
}
], - "queuedHypotheses": [
- {
- "queueId": "01JC4Z0000FILA00000000000001",
- "hypothesisId": "01JC4Z0000HIPOTESE000000001",
- "agenteAlvo": "dev-api",
- "hipotese": "The instructions don't say when the task is done.",
- "sugestao": "Add an explicit definition-of-done criterion.",
- "confiancaPercent": 72
}
], - "currentProfiles": [
- {
- "userId": "01JC4Z0000USUARIO0000000002",
- "competency": "TypeScript",
- "level": "advanced",
- "rationale": "Fixed three generic typing errors without help."
}
], - "instructions": [
- {
- "agent": "dev-api",
- "version": 4,
- "content": "# dev-api\n\n…"
}
], - "decisions": [
- {
- "actionType": "terminal",
- "status": "denied",
- "rejectionReason": "Command would delete the directory.",
- "decidedBy": "01JC4Z0000USUARIO0000000001",
- "decidedAt": "2026-07-27T13:00:00.000Z"
}
], - "windowFrom": "2026-07-27T12:00:00.000Z"
}Grava uma rodada da Anamnese com zero perfis para que a próxima comece em windowTo — sem isso, a janela nunca avançava e todo tick pagava uma rodada nova sobre o mesmo material.
| sessionId required | string |
| projectId required | string <uuid> |
| windowFrom required | string <date-time> |
| windowTo required | string <date-time> |
| eventCount required | number |
{- "projectId": "01JC4Z0000PROJETO0000000001",
- "windowFrom": "2026-07-27T12:00:00.000Z",
- "windowTo": "2026-07-27T12:15:00.000Z",
- "eventCount": 12
}{- "runId": "01JC4Z0000RODADA00000000001",
- "profiles": [
- { }
]
}O artefato É o evento artifact.c4_diagram: imutável, versionado e com autor, ao lado de artifact.module_map. Exige um module_map vigente — sem ele não há nível Container para desenhar (400).
| sessionId required | string |
| projectId required | string <uuid> |
| systemName required | string Nome do sistema/projeto, para o rótulo do diagrama. |
| systemDescription | string |
Lista de objects (C4AtorInternalDto) Atores externos do nível Context (Simon Brown). Os containers do nível Container NÃO entram aqui: vêm do module_map vigente do projeto. |
{- "projectId": "5a8591dd-4039-49df-9202-96385ba3eff8",
- "systemName": "Brabo",
- "systemDescription": "Agent-orchestrated engineering platform.",
- "actors": [
- {
- "name": "User",
- "type": "person",
- "description": "Whoever operates the product via the web."
}
]
}{- "diagrama": {
- "systemName": "Brabo",
- "systemDescription": "Agent-orchestrated engineering platform.",
- "actors": [
- {
- "name": "User",
- "type": "person",
- "description": "Whoever operates the product through the web."
}
], - "contextDiagram": "C4Context\n title Context Diagram -- Brabo\n ...",
- "containerDiagram": "C4Container\n title Container Diagram -- Brabo\n ..."
}, - "version": 1
}completed (com o veredito), failed (com a origem) ou dispensed (com a justificativa) — o lead nunca chama esta rota com desfecho pela metade: toda delegação já nasce aqui resolvida.
| sessionId required | string |
| projectId required | string <uuid> |
| area required | string A área que delegou — "qa" (Fase 8b) ou "infra" (Fase 8c). Texto, não enum. |
| taskId | string <uuid> A tarefa de backlog por trás da delegação, quando existe. QA (Fase 8b) sempre a manda; Infra (Fase 8c) nunca — a delegação é sobre a SESSÃO, não uma tarefa. |
| leadAgent required | string |
| subagent required | string |
| status required | string Valores: "completed" "failed" "dispensed" |
| parecerArtifactId | string Id do session_event |
| failureOrigin | string Valores: "infra" "modelo" "codigo" "politica" A ORIGEM da falha (ADR 0020/0038), nunca por eliminação — obrigatória quando |
| failureReason | string |
| justification | string Por que o lead escolheu NÃO delegar — obrigatório quando |
{- "projectId": "01JC4Z0000PROJETO0000000001",
- "area": "qa",
- "taskId": "01JC4Z0000TASK00000000001",
- "leadAgent": "qa-lead",
- "subagent": "qa-automacao",
- "status": "completed",
- "parecerArtifactId": "evt_01jc4z0000parecer000000001",
- "failureOrigin": "infra",
- "failureReason": "iteration limit reached without emit_perf_seguranca_verdict",
- "justification": "story with no relevant performance NFR"
}{- "id": "01JC4Z0000DELEGACAO00000001",
- "projectId": "01JC4Z0000PROJETO0000000001",
- "sessionId": "01JC4Z0000SESSAO000000000001",
- "taskId": "01JC4Z0000TAREFA00000000001",
- "area": "qa",
- "leadAgent": "qa-lead",
- "subagent": "qa-automacao",
- "status": "completed",
- "parecerArtifactId": "evt_01jc4z0000parecer000000001",
- "failureOrigin": null,
- "failureReason": null,
- "justification": null,
- "createdAt": "2026-07-30T12:00:00.000Z"
}Uma chamada só com tudo de que o prompt precisa: a história inteira, as regras de negócio resolvidas e os ADRs aplicáveis. module restringe os ADRs ao módulo daquele dev; ADR sem módulo declarado é TRANSVERSAL e sempre entra. Omitir module traz a coleção inteira, que é o que os gates de QA e SecOps querem ao reusar este mesmo contexto.
| projectId required | string |
| taskId required | string |
| module | string Exemplo: module=api |
{- "task": {
- "id": "01JC4Z0000TAREFA00000000001",
- "storyId": "01JC4Z0000HISTORIA000000001",
- "title": "POST /cart/items endpoint",
- "description": "Accepts SKU and quantity; validates stock.",
- "status": "in_progress",
- "assignedTo": "dev-api",
- "blocked": false,
- "blockedReason": null,
- "blockedOrigin": null,
- "gateStatus": null,
- "gateCorrectionCount": 0,
- "module": "api",
- "createdAt": "2026-07-25T10:00:00.000Z",
- "updatedAt": "2026-07-25T12:30:00.000Z"
}, - "story": {
- "id": "01JC4Z0000HISTORIA000000001",
- "epicId": "01JC4Z0000EPICO000000000001",
- "projectId": "01JC4Z0000PROJETO0000000001",
- "sessionId": "01JC4Z8QK3M7YV2N5T9B0PXHRA",
- "title": "Add item to cart",
- "description": "As a buyer, I want to gather items before paying.",
- "rf": [
- "The cart accepts up to 50 items"
], - "rnf": [
- "The response stays under 200 ms at p95"
], - "businessRuleIds": [
- "RN-014"
], - "dod": [
- "Unit tests green"
], - "dor": [
- "Module defined"
], - "moduleIds": [
- "api"
], - "status": "ready",
- "proposedReady": true,
- "returnedReason": "DoD too generic — spell out the acceptance criteria.",
- "returnedAt": "2026-08-02T14:00:00.000Z",
- "archivedAt": null,
- "archivedReason": null,
- "createdAt": "2026-07-25T09:00:00.000Z",
- "updatedAt": "2026-07-25T09:40:00.000Z"
}, - "businessRules": [
- {
- "title": "Cart accepts at most 50 items",
- "description": "Above that, the addition is refused with 409."
}
], - "adrs": [
- {
- "title": "0007 — Postgres queue instead of Redis",
- "content": "## Context\n\n…",
- "securityRelevant": false
}
], - "siblingTasks": [
- {
- "id": "01J0000000000000000000000",
- "title": "Expose POST /login returning a JWT",
- "status": "todo"
}
], - "moduleOpenTasks": [
- {
- "id": "01J0000000000000000000000",
- "title": "Expose POST /links",
- "status": "todo",
- "storyTitle": "Members create short links"
}
], - "moduleOpenTasksTotal": 0,
- "moduleContract": {
- "modulo": "board-engine",
- "expoe": [
- {
- "tipo": "funcao",
- "assinatura": "placePiece(board: Board, piece: Piece, pos: Pos): Board",
- "descricao": "Returns a new board; never mutates the one passed in."
}
]
}, - "primeiraDoModulo": true,
- "decisoes": [
- {
- "id": "string",
- "questionId": "string",
- "pergunta": "string",
- "decisao": "string",
- "decididaEm": "2019-08-24T14:15:22Z"
}
], - "decisoesDoProjeto": [
- {
- "id": "string",
- "questionId": "string",
- "pergunta": "string",
- "decisao": "string",
- "decididaEm": "2019-08-24T14:15:22Z"
}
]
}| sessionId required | string |
| projectId required | string <uuid> |
| title required | string |
| description | string |
{- "projectId": "01JC4Z0000PROJETO0000000001",
- "title": "Shopping cart",
- "description": "Everything the buyer does before paying."
}{- "id": "01JC4Z0000EPICO000000000001",
- "projectId": "01JC4Z0000PROJETO0000000001",
- "sessionId": "01JC4Z8QK3M7YV2N5T9B0PXHRA",
- "title": "Shopping cart",
- "description": "Everything the buyer does before paying.",
- "createdAt": "2026-07-25T08:30:00.000Z",
- "updatedAt": "2026-07-25T08:30:00.000Z"
}Usada para REIDRATAR o histórico de um agente conversacional e para ler os artefatos de que o kickoff dele precisa. A rota humana equivalente é protegida por RBAC; esta, pelo service token. latest=true devolve a CAUDA (ainda em seq crescente) e ignora afterSeq; types restringe a página a esses tipos de evento. A página tem teto de 200 em qualquer caso (ADR 0060).
| sessionId required | string |
| projectId required | string |
| afterSeq | string Exemplo: afterSeq=40 |
| limit | string Exemplo: limit=200 |
| latest | string Exemplo: latest=true Busca a cauda do log; ignora |
| types | string Exemplo: types=artifact.product_brief,artifact.business_rule Tipos de evento separados por vírgula (no máximo 20). Só eventos desses tipos contam para |
{- "items": [
- {
- "id": "01JC4Z8QK3M7YV2N5T9B0PXHRB",
- "sessionId": "01JC4Z8QK3M7YV2N5T9B0PXHRA",
- "seq": 41,
- "type": "chat.message",
- "actor": {
- "kind": "agent",
- "id": "dev-api"
}, - "payload": {
- "role": "assistant",
- "content": "I'll open the epic first."
}, - "createdAt": "2026-07-27T14:30:58.512Z"
}
], - "nextCursor": 41
}Mesmo caso de uso e mesma atribuição atômica de seq da rota humana. As hipóteses do Psicólogo NÃO passam por aqui: têm rota própria, com validação de evidência.
| sessionId required | string |
| projectId required | string <uuid> |
| type required | string Ver |
| actorKind required | string Valores: "user" "agent" "system" |
| actorId required | string Slug do agente que o emitiu. |
required | object |
{- "projectId": "01JC4Z0000PROJETO0000000001",
- "type": "agent.tool_call",
- "actorKind": "agent",
- "actorId": "dev-api",
- "payload": {
- "tool": "ler_arquivo"
}
}{- "id": "01JC4Z8QK3M7YV2N5T9B0PXHRB",
- "sessionId": "01JC4Z8QK3M7YV2N5T9B0PXHRA",
- "seq": 41,
- "type": "chat.message",
- "actor": {
- "kind": "agent",
- "id": "dev-api"
}, - "payload": {
- "role": "assistant",
- "content": "I'll open the epic first."
}, - "createdAt": "2026-07-27T14:30:58.512Z"
}O nextAction da resposta é o que o engine obedece: correct volta ao dev, run_secops avança o gate, done libera para o usuário, await_user estaciona o ciclo até a pergunta aberta ser respondida (RN-860, sem contar correção), e blocked significa que o teto de correções acabou. O MERGE nunca é automático — done só quer dizer que é a vez do humano.
| sessionId required | string |
| projectId required | string <uuid> |
| taskId required | string <uuid> |
| gate required | string Valores: "qa" "secops" |
| veredito required | string Valores: "approved" "changes_requested" "needs_user_decision"
|
| resumo required | string |
| itens required | Lista de strings O que precisa mudar, item a item. |
| pergunta | string Com |
| sugestao | string Com |
| openQuestionId | string <uuid> Com |
| maxCorrections | number Teto de idas e voltas antes de a tarefa virar |
{- "projectId": "01JC4Z0000PROJETO0000000001",
- "taskId": "01JC4Z0000TAREFA00000000001",
- "gate": "qa",
- "veredito": "changes_requested",
- "resumo": "Missing tests for the insufficient-stock path.",
- "itens": [
- "No test for a nonexistent SKU"
], - "pergunta": "Should a nonexistent code answer 404?",
- "sugestao": "404 with a plain message.",
- "openQuestionId": "4c8aa973-c398-4859-8f2e-3fe00dc96a12",
- "maxCorrections": 3
}{- "nextAction": "run_secops",
- "task": {
- "id": "01JC4Z0000TAREFA00000000001",
- "storyId": "01JC4Z0000HISTORIA000000001",
- "title": "POST /cart/items endpoint",
- "description": "Accepts SKU and quantity; validates stock.",
- "status": "in_progress",
- "assignedTo": "dev-api",
- "blocked": false,
- "blockedReason": null,
- "blockedOrigin": null,
- "gateStatus": null,
- "gateCorrectionCount": 0,
- "module": "api",
- "createdAt": "2026-07-25T10:00:00.000Z",
- "updatedAt": "2026-07-25T12:30:00.000Z"
}, - "openQuestionId": "4c8aa973-c398-4859-8f2e-3fe00dc96a12"
}Nasce offered. Quem aceita é uma PESSOA, pela rota humana — agente não ativa agente. Duas exceções. A oferta Criativo→PO que leva o product_brief pedido por um clique em "Estou pronto — a necessidade está validada" é aceita em nome de quem clicou, com essa pessoa como ator e implicito no payload (RN-658, ADR 0185). O handoff do PO ao Arquiteto é aceito pelo SISTEMA quando o backlog está coberto (ao menos uma regra de negócio, nenhuma sem história) e o repositório é local sem credencial de git no projeto (RN-660, ADR 0186); aceiteAutomatico diz se aconteceu e, se não, por quê. Em qualquer dos casos a resposta então traz status: accepted.
| sessionId required | string |
| projectId required | string <uuid> |
| fromAgent required | string |
| toAgent required | string |
| artifactId | string ULID do session_event do artefato que motivou o handoff. |
| seAusente | boolean Só se ninguém o pegou ainda (ADR 0182, RN-636): com uma oferta pendente ao alvo em QUALQUER sessão do projeto, devolve-a ( |
{- "projectId": "01JC4Z0000PROJETO0000000001",
- "fromAgent": "criativo",
- "toAgent": "po",
- "artifactId": "01JC4Z8QK3M7YV2N5T9B0PXHRB",
- "seAusente": true
}{- "id": "01JC4Z0000HANDOFF00000000001",
- "sessionId": "01JC4Z8QK3M7YV2N5T9B0PXHRA",
- "projectId": "01JC4Z0000PROJETO0000000001",
- "fromAgent": "criativo",
- "toAgent": "po",
- "artifactId": "01JC4Z0000ARTEFATO000000001",
- "status": "offered",
- "createdAt": "2026-07-24T10:00:00.000Z",
- "updatedAt": "2026-07-24T10:05:00.000Z",
- "desfecho": "criado",
- "aceiteAutomatico": {
- "aceito": true,
- "criterio": {
- "regras": 4,
- "cobertas": 4,
- "repositorio": "a_provisionar_local"
}, - "motivo": "regras_sem_historia"
}
}Cada hipótese precisa citar eventos que EXISTEM nesta sessão — evidência inventada é recusada com 400, e é isso que separa uma hipótese de uma opinião. A rodada anterior da sessão passa a superada.
| sessionId required | string |
| projectId required | string <uuid> |
| tier required | string Valores: "leve" "pesada" As duas faixas usam modelos de fato diferentes; o custo diverge. |
| triggeredBy required | string Valores: "auto" "manual" |
| eventCount required | number Tamanho do log analisado. |
| cause | string Valores: "normal" "timeout" "kill" "crash" "node_shutdown" "unknown" Causa CLASSIFICADA pelo engine. É ela, não o status terminal, que decide se |
required | Lista de objects (HypothesisDraftDto) |
{- "projectId": "01JC4Z0000PROJETO0000000001",
- "tier": "leve",
- "triggeredBy": "auto",
- "eventCount": 128,
- "cause": "timeout",
- "hypotheses": [
- {
- "agenteAlvo": "dev-api",
- "observacao": "The agent reopened the same task three times.",
- "hipotese": "The instructions don't say when the task is done.",
- "sugestao": "Add an explicit definition-of-done criterion.",
- "confiancaPercent": 72,
- "evidenceEventIds": [
- "01JC4Z8QK3M7YV2N5T9B0PXHRB"
], - "terminationAnalysis": {
- "causa": "heartbeat_timeout",
- "estadoDaSessao": "closed_abnormally",
- "analise": "The agent stopped responding after a 90s tool call."
}
}
]
}{- "analysisId": "01JC4Z0000ANALISE0000000001",
- "hypotheses": [
- {
- "id": "01JC4Z0000HIPOTESE000000001",
- "projectId": "01JC4Z0000PROJETO0000000001",
- "sessionId": "01JC4Z8QK3M7YV2N5T9B0PXHRA",
- "analysisId": "01JC4Z0000ANALISE0000000001",
- "agenteAlvo": "dev-api",
- "observacao": "The agent reopened the same task three times.",
- "hipotese": "The instructions don't say when to consider the task done.",
- "sugestao": "Add an explicit definition-of-done criterion to the instruction.",
- "confiancaPercent": 72,
- "evidenceEventIds": [
- "01JC4Z8QK3M7YV2N5T9B0PXHRB"
], - "terminationAnalysis": {
- "causa": "heartbeat_timeout",
- "estadoDaSessao": "closed_abnormally",
- "analise": "The agent stopped responding after a 90s tool call."
}, - "status": "proposed",
- "decidedBy": null,
- "decidedAt": null,
- "createdAt": "2026-07-26T18:00:00.000Z",
- "updatedAt": "2026-07-26T18:00:00.000Z"
}
]
}O conteúdo vem do próprio payload da proposed_action: artefato de infra nunca toca worktree, como os ADRs.
| prActionId required | string |
| projectId required | string |
{- "title": "api's production Dockerfile",
- "files": [
- {
- "path": "docker/api/Dockerfile.prod",
- "content": "FROM node:24.11.1-alpine3.21 AS build\n…"
}
]
}O module_map vigente mais os ADRs de infraestrutura relevantes, mais o roteamento de módulos do Arquiteto (ADR 0131) — as candidatas entre as quais o Infra Lead elege ao propor container_start.
| projectId required | string |
{- "moduleMap": {
- "id": "01JC4Z0000MODULEMAP00000001",
- "projectId": "01JC4Z0000PROJETO0000000001",
- "sessionId": "01JC4Z8QK3M7YV2N5T9B0PXHRA",
- "modules": [
- {
- "name": "api",
- "stack": "NestJS + Drizzle",
- "responsibility": "Business rules and HTTP surface.",
- "dependsOn": [
- "db"
], - "resources": {
- "cpus": 2,
- "memoryMb": 4096,
- "pidsLimit": 512
}
}
], - "version": 3,
- "createdAt": "2026-07-24T16:00:00.000Z"
}, - "adrs": [
- {
- "title": "0025 — Kubernetes deploy with Kustomize",
- "content": "## Context\n\n…"
}
], - "gitProvider": "github",
- "moduleRouting": {
- "status": "roteado",
- "roteamento": [
- {
- "modulo": "checkout-api",
- "imagemCandidata": "node:22-bookworm-slim",
- "porque": "This module is TypeScript on Node 22; the slim variant has the runtime and nothing else."
}
], - "version": 1,
- "eventId": "01JC4Z0000EVENTOROTEAMENTO01",
- "createdAt": "2026-08-30T12:00:00.000Z"
}, - "projectImage": {
- "image": "node:22-bookworm-slim",
- "rationale": "The module_map is all TypeScript over Node; the slim variant is enough and reduces surface.",
- "network": "none",
- "resources": {
- "cpus": 2,
- "memoryMb": 4096,
- "pidsLimit": 512
}
}
}Mesmo pipeline e mesma máquina de estados das PRs de dev.
| sessionId required | string |
| projectId required | string <uuid> |
| prActionId required | string <uuid> A ação |
| gate required | string Valores: "qa" "secops" |
| veredito required | string Valores: "approved" "changes_requested" |
| resumo required | string |
| itens required | Lista de strings |
| maxCorrections | number |
{- "projectId": "01JC4Z0000PROJETO0000000001",
- "prActionId": "01JC4Z8QK3M7YV2N5T9B0PXHRD",
- "gate": "secops",
- "veredito": "approved",
- "resumo": "Non-root image with no secret in ARG.",
- "itens": [ ],
- "maxCorrections": 3
}{- "nextAction": "done",
- "artifact": {
- "id": "01JC4Z0000INFRAARTEFATO00001",
- "projectId": "01JC4Z0000PROJETO0000000001",
- "sessionId": "01JC4Z8QK3M7YV2N5T9B0PXHRA",
- "title": "api's production Dockerfile",
- "prActionId": "01JC4Z8QK3M7YV2N5T9B0PXHRD",
- "gateStatus": "awaiting_qa",
- "gateCorrectionCount": 0,
- "blocked": false,
- "blockedReason": null,
- "createdAt": "2026-07-26T09:00:00.000Z",
- "updatedAt": "2026-07-26T09:20:00.000Z"
}
}Vira proposed_action, não escrita direta: mudar o comportamento de um agente é efeito externo e passa pelo mesmo pipeline de aprovação de todo o resto. Isso fecha o laço hipótese → patch.
| sessionId required | string |
| projectId required | string <uuid> |
| agent required | string |
| proposedContent required | string O arquivo INTEIRO como ficaria, não um diff. |
| rationale required | string |
| hypothesisId | string <uuid> A hipótese que motivou o patch. É isso que fecha o laço da Anamnese. |
{- "projectId": "01JC4Z0000PROJETO0000000001",
- "agent": "dev-api",
- "proposedContent": "# dev-api\n\nAlways run the suite before opening the PR.",
- "rationale": "The task was reopened three times for lack of a criterion.",
- "hypothesisId": "01JC4Z0000HIPOTESE000000001"
}{- "id": "01JC4Z8QK3M7YV2N5T9B0PXHRC",
- "projectId": "01JC4Z0000PROJETO0000000001",
- "sessionId": "01JC4Z8QK3M7YV2N5T9B0PXHRA",
- "seq": 7,
- "actionType": "terminal",
- "payload": {
- "command": "pnpm test"
}, - "status": "pending",
- "resolvedPolicy": "require_approval",
- "actor": {
- "kind": "agent",
- "id": "dev-api"
}, - "decidedBy": null,
- "decidedAt": null,
- "rejectionReason": null,
- "executionResult": null,
- "createdAt": "2026-07-27T14:33:10.900Z",
- "updatedAt": "2026-07-27T14:33:10.900Z"
}O metering é OBRIGATÓRIO: todo turno grava token_usage, e é isso que dá sentido ao orçamento. Não grava evento nenhum — quem narra o event log é o engine. Falha de provider volta em error com 200, porque a contabilidade em usage continua valendo: o turno gastou mesmo falhando.
| sessionId required | string |
| projectId required | string <uuid> |
| agentId | string Atribui o custo a este agente no painel do time. |
required | Lista de objects Histórico já montado pelo ContextManager do engine. |
Lista de objects Ferramentas oferecidas ao modelo; | |
| catalogoCompleto | boolean Pede o catálogo INTEIRO de ferramentas: o roteador de ferramentas do Jev (ADR 0179) é pulado nesta chamada. O engine o liga quando repete um passo cujo menu restrito fez o modelo responder sem chamar ferramenta. |
{- "projectId": "01JC4Z0000PROJETO0000000001",
- "agentId": "dev-api",
- "messages": [
- {
- "role": "user",
- "content": "Read file X."
}
], - "tools": [
- { }
], - "catalogoCompleto": true
}{- "message": {
- "role": "assistant",
- "content": "I'll read the file before answering.",
- "toolCalls": [
- {
- "id": "call_01",
- "name": "read_file",
- "arguments": {
- "path": "src/index.ts"
}
}
]
}, - "usage": {
- "inputTokens": 1820,
- "outputTokens": 340,
- "costMicros": 52700,
- "estimated": false
}, - "error": null,
- "errorCode": null,
- "modelName": "llama3.2:3b",
- "truncated": true,
- "toolRouting": {
- "modelo": "typesafe/jev-1.13",
- "ofertadas": 9,
- "menuAntes": [
- "string"
], - "menuDepois": [
- "string"
], - "escolha": "create_story",
- "confianca": 0.91,
- "segunda": {
- "opcao": "create_task",
- "probabilidade": 0.06
}, - "anterior": "read_file",
- "aplicado": true,
- "motivoDaQueda": "timeout",
- "origemDaQueda": "infra",
- "detalheDaQueda": { },
- "latenciaMs": 212,
- "custoMicros": 52,
- "gastoNaoRegistrado": true
}
}Mesma semântica de llm-turn, entregue quadro a quadro. O quadro done traz o usage — sem ele o turno sairia sem contabilidade.
| sessionId required | string |
| projectId required | string <uuid> |
| agentId | string |
required | Lista de objects |
Lista de objects | |
| catalogoCompleto | boolean Pede o catálogo INTEIRO de ferramentas: o roteador de ferramentas do Jev (ADR 0179) é pulado nesta chamada. O engine o liga quando repete um passo cujo menu restrito fez o modelo responder sem chamar ferramenta. |
{- "projectId": "01JC4Z0000PROJETO0000000001",
- "agentId": "criativo",
- "messages": [
- {
- "role": "user",
- "content": "Summarize what we decided."
}
], - "tools": [
- { }
], - "catalogoCompleto": true
}Recusa propor teto igual ou menor que o atual: a Anamnese roda periodicamente e repropria a mesma coisa a cada rodada, enchendo de ruído uma fila que o usuário precisa ler.
| sessionId required | string |
| projectId required | string <uuid> |
| area required | string A área cujo teto subiria. |
| proposto required | number >= 1 O teto proposto. Tem de ser MAIOR que o atual — propor o mesmo ou menos vira ruído numa fila que o usuário precisa ler, e a Anamnese roda periodicamente. |
| rationale required | string Ancorado em DECISÕES OBSERVADAS, não numa impressão. |
{- "projectId": "01JC4Z0000PROJETO0000000001",
- "area": "dev",
- "proposto": 4,
- "rationale": "You authorized the same request four times in this window, and none was denied."
}{- "id": "01JC4Z8QK3M7YV2N5T9B0PXHRC",
- "projectId": "01JC4Z0000PROJETO0000000001",
- "sessionId": "01JC4Z8QK3M7YV2N5T9B0PXHRA",
- "seq": 7,
- "actionType": "terminal",
- "payload": {
- "command": "pnpm test"
}, - "status": "pending",
- "resolvedPolicy": "require_approval",
- "actor": {
- "kind": "agent",
- "id": "dev-api"
}, - "decidedBy": null,
- "decidedAt": null,
- "rejectionReason": null,
- "executionResult": null,
- "createdAt": "2026-07-27T14:33:10.900Z",
- "updatedAt": "2026-07-27T14:33:10.900Z"
}O artefato É o evento artifact.module_contracts: imutável, versionado e com autor, ao lado de artifact.module_map. Cada chamada leva a lista INTEIRA e substitui a versão anterior. O que um módulo consome não se escreve aqui: é o dependsOn do module_map vigente.
| sessionId required | string |
| projectId required | string <uuid> |
required | Lista de objects (ContratoDeModuloInternalDto) A lista INTEIRA de contratos: cada chamada é uma versão nova que substitui a anterior. O que um módulo CONSOME não está aqui — é derivado do |
{- "projectId": "5a8591dd-4039-49df-9202-96385ba3eff8",
- "contratos": [
- {
- "modulo": "board-engine",
- "expoe": [
- {
- "tipo": "funcao",
- "assinatura": "placePiece(board: Board, piece: Piece, pos: Pos): Board",
- "descricao": "Returns a new board; never mutates the one passed in."
}
]
}
]
}{- "contratos": [
- {
- "modulo": "board-engine",
- "expoe": [
- {
- "tipo": "funcao",
- "assinatura": "placePiece(board: Board, piece: Piece, pos: Pos): Board",
- "descricao": "Returns a new board; never mutates the one passed in."
}
]
}
], - "version": 1
}O histórico é imutável: cada publicação é uma versão a mais, e a vigente é a de maior version. CICLO de dependência entre módulos faz o mapa ser RECUSADO com 400 — o grafo precisa ser acíclico.
| sessionId required | string |
| projectId required | string <uuid> |
required | Lista de objects Grafo de módulos. Um CICLO faz o mapa ser recusado com 400 — a validação é de domínio. Cada módulo pode declarar |
{- "projectId": "01JC4Z0000PROJETO0000000001",
- "modules": [
- {
- "name": "api",
- "stack": "NestJS",
- "responsibility": "Rules and HTTP",
- "dependsOn": [
- "db"
], - "resources": {
- "cpus": 1,
- "memoryMb": 1024,
- "pidsLimit": 256
}
}
]
}{- "id": "01JC4Z0000MODULEMAP00000001",
- "projectId": "01JC4Z0000PROJETO0000000001",
- "sessionId": "01JC4Z8QK3M7YV2N5T9B0PXHRA",
- "modules": [
- {
- "name": "api",
- "stack": "NestJS + Drizzle",
- "responsibility": "Business rules and HTTP surface.",
- "dependsOn": [
- "db"
], - "resources": {
- "cpus": 2,
- "memoryMb": 4096,
- "pidsLimit": 512
}
}
], - "version": 3,
- "createdAt": "2026-07-24T16:00:00.000Z"
}O artefato É o evento artifact.module_routing: imutável, versionado e com autor, ao lado de artifact.module_map/artifact.project_image/artifact.c4_diagram. Cada item é validado com a mesma régua de choose_project_image (tag/digest explícito, latest recusado, rationale não trivial); nome de módulo fora do module_map vigente, lista vazia ou módulo repetido são recusados com 400.
| sessionId required | string |
| projectId required | string <uuid> |
required | Lista de objects (RoteamentoDeModuloInternalDto) Um item por módulo do module_map vigente. A imagem candidata é o Arquiteto propondo, não decidindo — a Infra elege entre as candidatas num passo posterior. |
{- "projectId": "5a8591dd-4039-49df-9202-96385ba3eff8",
- "roteamento": [
- {
- "modulo": "checkout-api",
- "imagemCandidata": "node:22-bookworm-slim",
- "porque": "This module is TypeScript on Node 22; the slim variant has the runtime and nothing else."
}
]
}{- "roteamento": [
- {
- "modulo": "checkout-api",
- "imagemCandidata": "node:22-bookworm-slim",
- "porque": "This module is TypeScript on Node 22; the slim variant has the runtime and nothing else."
}
], - "version": 1
}Consultada pelo SessionServer do engine no timeout de heartbeat. pending: true faz o timeout ser reagendado em vez de fechar, e o motivo vai para o log do engine — sessão que não fecha sem dizer por quê é indiagnosticável.
| sessionId required | string |
{- "pending": true,
- "motivo": "handoff po → arquiteto aguardando aceite",
- "aguardandoUsuarioDesde": "2026-09-18T12:00:00.000Z"
}Competência fora do catálogo é RECUSADA, e evidência que cita evento de outro projeto também — as duas validações existem porque o modelo erra as duas coisas.
| sessionId required | string |
| projectId required | string <uuid> |
| windowFrom required | string <date-time> |
| windowTo required | string <date-time> |
| eventCount required | number |
required | Lista de objects (ProficiencyDraftDto) |
{- "projectId": "01JC4Z0000PROJETO0000000001",
- "windowFrom": "2026-07-27T12:00:00.000Z",
- "windowTo": "2026-07-27T12:15:00.000Z",
- "eventCount": 240,
- "profiles": [
- {
- "userId": "01JC4Z0000USUARIO0000000002",
- "competency": "TypeScript",
- "level": "avancado",
- "rationale": "Fixed three generic typing errors without help.",
- "evidenceEventIds": [
- "01JC4Z8QK3M7YV2N5T9B0PXHRB"
]
}
]
}{- "runId": "01JC4Z0000RODADA00000000001",
- "profiles": [
- { }
]
}O artefato É o evento artifact.project_image: imutável, versionado e com autor. Revisar é emitir versão nova, nunca sobrescrever. Enquanto não houver uma, o container do projeto não sobe e a aba Código responde 409.
| sessionId required | string |
| projectId required | string <uuid> |
| image required | string Referência OCI com TAG ou digest explícito. |
| rationale required | string Por que esta imagem. Mínimo de 10 caracteres. |
| network | string Padrão: "none" Valores: "none" "egress" A postura de rede do container. |
object Teto de recursos. Acima do máximo, a decisão é RECUSADA, nunca rebaixada em silêncio. |
{- "projectId": "5a8591dd-4039-49df-9202-96385ba3eff8",
- "image": "node:22-bookworm-slim",
- "rationale": "The module_map is TypeScript on Node 22; the slim variant has the runtime and nothing else.",
- "network": "none",
- "resources": {
- "cpus": 2,
- "memoryMb": 4096,
- "pidsLimit": 512
}
}{- "decisao": {
- "image": "node:22-bookworm-slim",
- "rationale": "The module_map is all TypeScript over Node; the slim variant is enough and reduces surface.",
- "network": "none",
- "resources": {
- "cpus": 2,
- "memoryMb": 4096,
- "pidsLimit": 512
}
}, - "version": 1,
- "containerDePe": {
- "status": "running",
- "imageVersion": 2
}
}alreadyAnalyzed é o que dá IDEMPOTÊNCIA ao caminho automático: com true o worker encerra cedo sem gastar token. As hipóteses anteriores vêm junto para a rodada não se repetir.
| sessionId required | string |
| projectId required | string |
{- "alreadyAnalyzed": false,
- "sessionStatus": "closed_abnormally",
- "terminationReason": "heartbeat_timeout",
- "businessRules": [
- {
- "id": "RN-014",
- "title": "Cart accepts at most 50 items"
}
], - "priorHypotheses": [
- {
- "agenteAlvo": "dev-api",
- "hipotese": "The instructions don't say when the task is done.",
- "sugestao": "Add an explicit definition-of-done criterion.",
- "confiancaPercent": 72
}
]
}Vetoriza o título e os títulos das regras do projeto (as 100 mais recentes) com o modelo de embedding do RAG e avisa com cosseno ≥ o limiar. Nunca recusa. Sem provider de embedding a checagem é PULADA com o motivo, narrado também no event log. O gasto de embedding é medido como linha própria de token_usage (ator system/duplicata-semantica).
| sessionId required | string |
| projectId required | string <uuid> |
| kind required | string Valor: "business_rule" |
| title required | string Título da regra RECÉM-acrescentada. Regras com o mesmo título normalizado (a RN-080 faz dela a nova) ficam fora da comparação. |
{- "projectId": "01JC4Z0000PROJETO0000000001",
- "kind": "business_rule",
- "title": "Greeting with the caller name"
}{- "status": "warned",
- "similarTo": {
- "id": "01JC4Z0000HISTORIA000000001",
- "title": "Deterministic public greeting endpoint",
- "similarity": 0.83
}, - "closest": {
- "id": "01JC4Z0000HISTORIA000000001",
- "title": "Deterministic public greeting endpoint",
- "similarity": 0.83
}, - "similarity": 0.83,
- "threshold": 0.8,
- "compared": 12,
- "total": 12,
- "reason": "provider \"ollama\" did not answer",
- "message": "string"
}businessRuleIds é o que alimenta a cobertura regra→história. Cada id tem de referenciar um evento artifact.business_rule que EXISTE — a validação recusa id inventado. A resposta traz também semanticDuplicate (RN-681): um AVISO por embedding contra as histórias do projeto, nunca uma recusa.
| sessionId required | string |
| projectId required | string <uuid> |
| epicId required | string <uuid> |
| title required | string |
| description | string |
| rf | Lista de strings |
| rnf | Lista de strings |
| dod | Lista de strings |
| dor | Lista de strings |
| businessRuleIds | Lista de strings Cada id precisa referenciar um evento |
{- "projectId": "01JC4Z0000PROJETO0000000001",
- "epicId": "01JC4Z0000EPICO000000000001",
- "title": "Add item to cart",
- "description": "As a buyer, I want to gather items before paying.",
- "rf": [
- "The cart accepts up to 50 items"
], - "rnf": [
- "Response under 200ms at p95"
], - "dod": [
- "Unit tests green"
], - "dor": [
- "Module defined"
], - "businessRuleIds": [
- "RN-014"
]
}{- "id": "01JC4Z0000HISTORIA000000001",
- "epicId": "01JC4Z0000EPICO000000000001",
- "projectId": "01JC4Z0000PROJETO0000000001",
- "sessionId": "01JC4Z8QK3M7YV2N5T9B0PXHRA",
- "title": "Add item to cart",
- "description": "As a buyer, I want to gather items before paying.",
- "rf": [
- "The cart accepts up to 50 items"
], - "rnf": [
- "The response stays under 200 ms at p95"
], - "businessRuleIds": [
- "RN-014"
], - "dod": [
- "Unit tests green"
], - "dor": [
- "Module defined"
], - "moduleIds": [
- "api"
], - "status": "ready",
- "proposedReady": true,
- "returnedReason": "DoD too generic — spell out the acceptance criteria.",
- "returnedAt": "2026-08-02T14:00:00.000Z",
- "archivedAt": null,
- "archivedReason": null,
- "createdAt": "2026-07-25T09:00:00.000Z",
- "updatedAt": "2026-07-25T09:40:00.000Z",
- "semanticDuplicate": {
- "status": "warned",
- "similarTo": {
- "id": "01JC4Z0000HISTORIA000000001",
- "title": "Deterministic public greeting endpoint",
- "similarity": 0.83
}, - "closest": {
- "id": "01JC4Z0000HISTORIA000000001",
- "title": "Deterministic public greeting endpoint",
- "similarity": 0.83
}, - "similarity": 0.83,
- "threshold": 0.8,
- "compared": 12,
- "total": 12,
- "reason": "provider \"ollama\" did not answer",
- "message": "string"
}
}A história e suas tarefas saem do backlog, da cobertura, do plano e do claim; a linha fica e backlog.story_archived a registra, com o PO como ator (RN-727).
| storyId required | string |
| projectId required | string <uuid> |
| reason | string |
{- "projectId": "01JC4Z0000PROJETO0000000001",
- "reason": "Duplicate of \"Register a user\"."
}{- "id": "01JC4Z0000HISTORIA000000001",
- "epicId": "01JC4Z0000EPICO000000000001",
- "projectId": "01JC4Z0000PROJETO0000000001",
- "sessionId": "01JC4Z8QK3M7YV2N5T9B0PXHRA",
- "title": "Add item to cart",
- "description": "As a buyer, I want to gather items before paying.",
- "rf": [
- "The cart accepts up to 50 items"
], - "rnf": [
- "The response stays under 200 ms at p95"
], - "businessRuleIds": [
- "RN-014"
], - "dod": [
- "Unit tests green"
], - "dor": [
- "Module defined"
], - "moduleIds": [
- "api"
], - "status": "ready",
- "proposedReady": true,
- "returnedReason": "DoD too generic — spell out the acceptance criteria.",
- "returnedAt": "2026-08-02T14:00:00.000Z",
- "archivedAt": null,
- "archivedReason": null,
- "createdAt": "2026-07-25T09:00:00.000Z",
- "updatedAt": "2026-07-25T09:40:00.000Z"
}businessRuleIds é SOMADO às regras já ligadas; listas não vazias substituem as atuais e listas omitidas ficam. Depois de gravar, a história é promovida pelo MESMO critério da criação de história (ready no modo auto, proposedReady no manual) (RN-720).
| sessionId required | string |
| storyId required | string |
| projectId required | string <uuid> |
| description | string |
| rf | Lista de strings |
| rnf | Lista de strings |
| dod | Lista de strings |
| dor | Lista de strings |
| businessRuleIds | Lista de strings SOMADO às regras que a história já cita (completar nunca desliga uma regra); cada id novo tem de referenciar um |
| questionId | string <uuid> RN-859 (ADR 0214): uma pergunta aberta que o usuário JÁ respondeu — só então uma história |
{- "projectId": "01JC4Z0000PROJETO0000000001",
- "description": "As a buyer, I want to pay by card.",
- "rf": [
- "The cart accepts up to 50 items"
], - "rnf": [
- "Response under 200ms at p95"
], - "dod": [
- "Unit tests green"
], - "dor": [
- "Module defined"
], - "businessRuleIds": [
- "RN-014"
], - "questionId": "ffa0420e-f70b-4e5b-99a0-434bff860467"
}{- "id": "01JC4Z0000HISTORIA000000001",
- "epicId": "01JC4Z0000EPICO000000000001",
- "projectId": "01JC4Z0000PROJETO0000000001",
- "sessionId": "01JC4Z8QK3M7YV2N5T9B0PXHRA",
- "title": "Add item to cart",
- "description": "As a buyer, I want to gather items before paying.",
- "rf": [
- "The cart accepts up to 50 items"
], - "rnf": [
- "The response stays under 200 ms at p95"
], - "businessRuleIds": [
- "RN-014"
], - "dod": [
- "Unit tests green"
], - "dor": [
- "Module defined"
], - "moduleIds": [
- "api"
], - "status": "ready",
- "proposedReady": true,
- "returnedReason": "DoD too generic — spell out the acceptance criteria.",
- "returnedAt": "2026-08-02T14:00:00.000Z",
- "archivedAt": null,
- "archivedReason": null,
- "createdAt": "2026-07-25T09:00:00.000Z",
- "updatedAt": "2026-07-25T09:40:00.000Z"
}Só história draft sem tarefa em execução; grava backlog.story_updated com o PO como ator (RN-727). Uma ready também, quando questionId cita uma pergunta aberta que o usuário já respondeu (RN-859).
| storyId required | string |
| projectId required | string <uuid> |
| title | string |
| description | string |
| questionId | string <uuid> RN-859 (ADR 0214): uma pergunta aberta que o usuário JÁ respondeu — só então uma história |
{- "projectId": "01JC4Z0000PROJETO0000000001",
- "title": "Register a user",
- "description": "As a visitor, I want to sign up.",
- "questionId": "ffa0420e-f70b-4e5b-99a0-434bff860467"
}{- "id": "01JC4Z0000HISTORIA000000001",
- "epicId": "01JC4Z0000EPICO000000000001",
- "projectId": "01JC4Z0000PROJETO0000000001",
- "sessionId": "01JC4Z8QK3M7YV2N5T9B0PXHRA",
- "title": "Add item to cart",
- "description": "As a buyer, I want to gather items before paying.",
- "rf": [
- "The cart accepts up to 50 items"
], - "rnf": [
- "The response stays under 200 ms at p95"
], - "businessRuleIds": [
- "RN-014"
], - "dod": [
- "Unit tests green"
], - "dor": [
- "Module defined"
], - "moduleIds": [
- "api"
], - "status": "ready",
- "proposedReady": true,
- "returnedReason": "DoD too generic — spell out the acceptance criteria.",
- "returnedAt": "2026-08-02T14:00:00.000Z",
- "archivedAt": null,
- "archivedReason": null,
- "createdAt": "2026-07-25T09:00:00.000Z",
- "updatedAt": "2026-07-25T09:40:00.000Z"
}Esta é a validação cruzada: módulo que não existe no mapa vigente vira pendência de arquitetura em vez de passar despercebido.
| sessionId required | string |
| projectId required | string <uuid> |
| storyId required | string <uuid> |
| moduleIds required | Lista de strings Nomes do |
{- "projectId": "01JC4Z0000PROJETO0000000001",
- "storyId": "01JC4Z0000HISTORIA000000001",
- "moduleIds": [
- "api",
- "web"
]
}{- "id": "01JC4Z0000HISTORIA000000001",
- "epicId": "01JC4Z0000EPICO000000000001",
- "projectId": "01JC4Z0000PROJETO0000000001",
- "sessionId": "01JC4Z8QK3M7YV2N5T9B0PXHRA",
- "title": "Add item to cart",
- "description": "As a buyer, I want to gather items before paying.",
- "rf": [
- "The cart accepts up to 50 items"
], - "rnf": [
- "The response stays under 200 ms at p95"
], - "businessRuleIds": [
- "RN-014"
], - "dod": [
- "Unit tests green"
], - "dor": [
- "Module defined"
], - "moduleIds": [
- "api"
], - "status": "ready",
- "proposedReady": true,
- "returnedReason": "DoD too generic — spell out the acceptance criteria.",
- "returnedAt": "2026-08-02T14:00:00.000Z",
- "archivedAt": null,
- "archivedReason": null,
- "createdAt": "2026-07-25T09:00:00.000Z",
- "updatedAt": "2026-07-25T09:40:00.000Z"
}| sessionId required | string |
| projectId required | string <uuid> |
| storyId required | string <uuid> |
| title required | string |
| description | string |
{- "projectId": "01JC4Z0000PROJETO0000000001",
- "storyId": "01JC4Z0000HISTORIA000000001",
- "title": "Endpoint POST /cart/items",
- "description": "Accepts SKU and quantity; validates stock."
}{- "id": "01JC4Z0000TAREFA00000000001",
- "storyId": "01JC4Z0000HISTORIA000000001",
- "title": "POST /cart/items endpoint",
- "description": "Accepts SKU and quantity; validates stock.",
- "status": "in_progress",
- "assignedTo": "dev-api",
- "blocked": false,
- "blockedReason": null,
- "blockedOrigin": null,
- "gateStatus": null,
- "gateCorrectionCount": 0,
- "module": "api",
- "createdAt": "2026-07-25T10:00:00.000Z",
- "updatedAt": "2026-07-25T12:30:00.000Z"
}Não há desbloqueio automático: quem desbloqueia é uma pessoa, pela rota humana. É isso que impede um agente de girar indefinidamente.
| sessionId required | string |
| taskId required | string |
| projectId required | string <uuid> |
| agentId required | string |
| reason required | string |
| diagnosis required | string Diagnóstico em texto livre — um detalhe humano do que aconteceu, não a origem estruturada da falha. |
| origin | string Valores: "infra" "modelo" "codigo" "politica" A ORIGEM da falha: |
{- "projectId": "01JC4Z0000PROJETO0000000001",
- "agentId": "dev-api",
- "reason": "Gate correction cap reached.",
- "diagnosis": "Correction cycle exhausted without exit 0.",
- "origin": "infra"
}{- "id": "01JC4Z0000TAREFA00000000001",
- "storyId": "01JC4Z0000HISTORIA000000001",
- "title": "POST /cart/items endpoint",
- "description": "Accepts SKU and quantity; validates stock.",
- "status": "in_progress",
- "assignedTo": "dev-api",
- "blocked": false,
- "blockedReason": null,
- "blockedOrigin": null,
- "gateStatus": null,
- "gateCorrectionCount": 0,
- "module": "api",
- "createdAt": "2026-07-25T10:00:00.000Z",
- "updatedAt": "2026-07-25T12:30:00.000Z"
}Chamada quando a PR do dev agent está pronta para revisão.
| sessionId required | string |
| taskId required | string |
| projectId required | string <uuid> |
| agentId required | string |
{- "projectId": "01JC4Z0000PROJETO0000000001",
- "agentId": "dev-api"
}{- "gateStatus": "awaiting_qa"
}| sessionId required | string |
| taskId required | string |
| projectId required | string <uuid> |
| agentId required | string Só o agente que reivindicou a tarefa pode movê-la. |
| status required | string Valores: "todo" "in_progress" "in_review" "done" |
{- "projectId": "01JC4Z0000PROJETO0000000001",
- "agentId": "dev-api",
- "status": "in_review"
}{- "id": "01JC4Z0000TAREFA00000000001",
- "storyId": "01JC4Z0000HISTORIA000000001",
- "title": "POST /cart/items endpoint",
- "description": "Accepts SKU and quantity; validates stock.",
- "status": "in_progress",
- "assignedTo": "dev-api",
- "blocked": false,
- "blockedReason": null,
- "blockedOrigin": null,
- "gateStatus": null,
- "gateCorrectionCount": 0,
- "module": "api",
- "createdAt": "2026-07-25T10:00:00.000Z",
- "updatedAt": "2026-07-25T12:30:00.000Z"
}Chamada pelo job de retentativa do engine (até 3 vezes, depois de 1, 3 e 10 min). Mesmo caso de uso do desbloqueio humano, com ator system; tarefa que não está mais bloqueada fica intocada.
| sessionId required | string |
| taskId required | string |
| projectId required | string <uuid> |
| agentId required | string O dev agent cuja falha transitória agendou a retentativa. |
{- "projectId": "01JC4Z0000PROJETO0000000001",
- "agentId": "dev-api"
}{- "id": "01JC4Z0000TAREFA00000000001",
- "storyId": "01JC4Z0000HISTORIA000000001",
- "title": "POST /cart/items endpoint",
- "description": "Accepts SKU and quantity; validates stock.",
- "status": "in_progress",
- "assignedTo": "dev-api",
- "blocked": false,
- "blockedReason": null,
- "blockedOrigin": null,
- "gateStatus": null,
- "gateCorrectionCount": 0,
- "module": "api",
- "createdAt": "2026-07-25T10:00:00.000Z",
- "updatedAt": "2026-07-25T12:30:00.000Z"
}ATÔMICA por desenho: com vários dev agents no mesmo módulo, dois claims concorrentes não podem devolver a mesma tarefa. Sem tarefa disponível, devolve vazio em vez de erro.
| sessionId required | string |
| projectId required | string <uuid> |
| module required | string Só tarefas deste módulo entram no sorteio. |
| agentId required | string |
{- "projectId": "01JC4Z0000PROJETO0000000001",
- "module": "api",
- "agentId": "dev-api"
}{- "id": "01JC4Z0000TAREFA00000000001",
- "storyId": "01JC4Z0000HISTORIA000000001",
- "title": "POST /cart/items endpoint",
- "description": "Accepts SKU and quantity; validates stock.",
- "status": "in_progress",
- "assignedTo": "dev-api",
- "blocked": false,
- "blockedReason": null,
- "blockedOrigin": null,
- "gateStatus": null,
- "gateCorrectionCount": 0,
- "module": "api",
- "createdAt": "2026-07-25T10:00:00.000Z",
- "updatedAt": "2026-07-25T12:30:00.000Z"
}Não bloqueadas e fora dos gates de PR; vazio a menos que a sessão seja de execução. Usada pelo engine ao parar uma execução, para bloquear as que nenhum dev agent vivo segura.
| sessionId required | string |
[- {
- "projectId": "5a8591dd-4039-49df-9202-96385ba3eff8",
- "taskId": "e6e9d88a-9b63-468a-aec3-b7a11de27af8",
- "assignedTo": "dev-api"
}
]Só chegam aqui os encerramentos que a api NÃO causou — crash, kill, heartbeat_timeout, desligamento defensivo. Uma parada planejada pela própria api não passa por aqui: o engine já soube dela pela outbox, e relatá-la de volta seria eco.
| sessionId required | string |
| projectId required | string <uuid> |
| to required | string Valores: "closing" "closed" "closed_abnormally"
|
| reason | string A ORIGEM do encerramento, não um diagnóstico por eliminação. |
{- "projectId": "01JC4Z0000PROJETO0000000001",
- "to": "closed_abnormally",
- "reason": "heartbeat_timeout"
}{- "id": "01JC4Z8QK3M7YV2N5T9B0PXHRA",
- "projectId": "01JC4Z0000PROJETO0000000001",
- "createdBy": "01JC4Z0000USUARIO0000000001",
- "status": "active",
- "kind": "criativa",
- "name": "Cart checkout",
- "nextSeq": 42,
- "createdAt": "2026-07-27T14:03:22.187Z",
- "updatedAt": "2026-07-27T14:31:09.004Z",
- "closedAt": null,
- "terminationReason": null,
- "traceParent": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
}A camada de PROVIDER das capabilities (ADR 0041) para os nove providers, lida das mesmas instâncias que servem as chamadas. Uma capability só é true quando provada contra a API real. A tela lê routingPreference daqui antes de oferecer preferência de roteamento num binding (ADR 0166).
[- {
- "provider": "openrouter",
- "capabilities": {
- "streaming": true,
- "toolCalling": true,
- "listModels": true,
- "embeddings": false,
- "routingPreference": false,
- "stickySession": false,
- "priceCeiling": false,
- "promptCaching": false
}
}
]Cascata agente → área → projeto → workspace, SEM sessão: é a configuração do agente no projeto, não a de uma conversa específica. origin: "agent" quer dizer que este agente DIVERGIU do padrão da área; qualquer outra origem quer dizer que ele herda.
| projectId required | string |
| agentSlug required | string Exemplo: dev-api |
{- "modelId": "01JC4Z0000MODELO00000000001",
- "origin": "project",
- "routingPreference": null,
- "skipped": [
- {
- "scope": "agent",
- "modelId": "01JC4Z0000MODELO00000000001",
- "reason": "unavailable"
}
], - "herdadoDoLead": "secops"
}Vale para toda sessão deste projeto que não tem binding próprio, e só para elas — antes do ADR 0064 o binding era global e alcançava outros projetos também. É assim que se dá um modelo barato ao Psicólogo e um caro ao Arquiteto no mesmo projeto, e também é como um agente DIVERGE do padrão da área.
| projectId required | string |
| agentSlug required | string Exemplo: dev-api |
| modelId required | string <uuid> Modelo a fixar neste escopo. Tem de existir em |
| routingPreference | string or null Valores: "price" "throughput" "latency" Como um HUB escolhe o upstream que serve o modelo (ADR 0166, RN-583). AUSENTE mantém o valor gravado quando o provider do modelo novo o aceita, e o limpa caso contrário; |
{- "modelId": "9b1c2d3e-4f50-4a61-8b72-0c3d4e5f6a7b",
- "routingPreference": "throughput"
}{- "id": "01JC4Z0000BINDING00000000001",
- "scope": "project",
- "scopeId": "01JC4Z0000PROJETO0000000001",
- "modelId": "01JC4Z0000MODELO00000000001",
- "routingPreference": null,
- "createdBy": "01JC4Z0000USUARIO0000000001",
- "createdAt": "2026-07-11T09:00:00.000Z",
- "updatedAt": "2026-07-11T09:00:00.000Z"
}APAGA o binding do agente — não copia o modelo da área para ele. Copiar pareceria igual na tela e não é: viraria uma cópia, e a próxima mudança da área deixaria este agente para trás em silêncio. 404 quando ele já herda.
| projectId required | string |
| agentSlug required | string Exemplo: dev-api |
Janela DESLIZANTE de 30 dias, não o mês civil corrente: é o rótulo que a tela mostra, e com mês civil a estimativa despencaria no dia 1º por virar a página, não por mudança de uso. Só conta actor_kind = agent (RN-038). Agente que nunca rodou não aparece na lista — a tela mostra um traço para ele, o que é diferente de zero.
| projectId required | string |
[- {
- "actorId": "dev-api",
- "costMicros": 3400000,
- "inputTokens": 128000,
- "outputTokens": 14200
}
]O padrão que o lead e os subagentes da área compartilham. origin: "area" quer dizer que alguém o escolheu para esta área; qualquer outra origem quer dizer que a própria área herda do projeto ou do workspace.
| projectId required | string |
| areaKey required | string Exemplo: qa |
{- "modelId": "01JC4Z0000MODELO00000000001",
- "origin": "project",
- "routingPreference": null,
- "skipped": [
- {
- "scope": "agent",
- "modelId": "01JC4Z0000MODELO00000000001",
- "reason": "unavailable"
}
], - "herdadoDoLead": "secops"
}Vale para o lead e para todo subagente da área que não tem binding próprio (RN-102). Exige maintainer porque alcança a área inteira de uma vez, e escolher modelo é decidir gasto — o mesmo motivo do teto de paralelismo.
| projectId required | string |
| areaKey required | string Exemplo: qa |
| modelId required | string <uuid> Modelo a fixar neste escopo. Tem de existir em |
| routingPreference | string or null Valores: "price" "throughput" "latency" Como um HUB escolhe o upstream que serve o modelo (ADR 0166, RN-583). AUSENTE mantém o valor gravado quando o provider do modelo novo o aceita, e o limpa caso contrário; |
{- "modelId": "9b1c2d3e-4f50-4a61-8b72-0c3d4e5f6a7b",
- "routingPreference": "throughput"
}{- "id": "01JC4Z0000BINDING00000000001",
- "scope": "project",
- "scopeId": "01JC4Z0000PROJETO0000000001",
- "modelId": "01JC4Z0000MODELO00000000001",
- "routingPreference": null,
- "createdBy": "01JC4Z0000USUARIO0000000001",
- "createdAt": "2026-07-11T09:00:00.000Z",
- "updatedAt": "2026-07-11T09:00:00.000Z"
}A área deixa de ter padrão próprio e passa a herdar do projeto (ou do workspace). Agentes que divergiram continuam divergindo: o binding deles é outro, e apagar este não pode decidir por eles.
| projectId required | string |
| areaKey required | string Exemplo: qa |
Valores em micro-USD. Exige maintainer — é informação de custo.
| projectId required | string |
{- "id": "01JC4Z0000ORCAMENTO000000001",
- "projectId": "01JC4Z0000PROJETO0000000001",
- "sessionId": null,
- "limitMicros": 50000000,
- "spentMicros": 12400000,
- "policy": "block",
- "lastThresholdNotified": 70,
- "createdAt": "2026-07-12T08:00:00.000Z",
- "updatedAt": "2026-07-27T15:00:00.000Z"
}O limite entra em DÓLARES e é convertido para micro-USD. Com policy=block a chamada que estouraria o teto é RECUSADA, não só registrada.
| projectId required | string |
| limitUsd required | number >= 0 Teto em DÓLARES — o único lugar do contrato que fala em dólares, porque é o que uma pessoa digita. A api o converte para micro-USD, e as respostas voltam em micro-USD. |
| policy required | string Valores: "block" "allow"
|
{- "limitUsd": 50,
- "policy": "block"
}{- "id": "01JC4Z0000ORCAMENTO000000001",
- "projectId": "01JC4Z0000PROJETO0000000001",
- "sessionId": null,
- "limitMicros": 50000000,
- "spentMicros": 12400000,
- "policy": "block",
- "lastThresholdNotified": 70,
- "createdAt": "2026-07-12T08:00:00.000Z",
- "updatedAt": "2026-07-27T15:00:00.000Z"
}O binding CRU deste escopo — null se o projeto só herda do workspace.
| projectId required | string |
{- "id": "01JC4Z0000BINDING00000000001",
- "scope": "project",
- "scopeId": "01JC4Z0000PROJETO0000000001",
- "modelId": "01JC4Z0000MODELO00000000001",
- "routingPreference": null,
- "createdBy": "01JC4Z0000USUARIO0000000001",
- "createdAt": "2026-07-11T09:00:00.000Z",
- "updatedAt": "2026-07-11T09:00:00.000Z"
}| projectId required | string |
| modelId required | string <uuid> Modelo a fixar neste escopo. Tem de existir em |
| routingPreference | string or null Valores: "price" "throughput" "latency" Como um HUB escolhe o upstream que serve o modelo (ADR 0166, RN-583). AUSENTE mantém o valor gravado quando o provider do modelo novo o aceita, e o limpa caso contrário; |
{- "modelId": "9b1c2d3e-4f50-4a61-8b72-0c3d4e5f6a7b",
- "routingPreference": "throughput"
}{- "id": "01JC4Z0000BINDING00000000001",
- "scope": "project",
- "scopeId": "01JC4Z0000PROJETO0000000001",
- "modelId": "01JC4Z0000MODELO00000000001",
- "routingPreference": null,
- "createdBy": "01JC4Z0000USUARIO0000000001",
- "createdAt": "2026-07-11T09:00:00.000Z",
- "updatedAt": "2026-07-11T09:00:00.000Z"
}Forma em lote de GET .../agent-bindings/:agentSlug e GET .../area-bindings/:areaKey: cada chave pedida recebe EXATAMENTE o valor que a rota de chave única dela responde (mesma cascata, mesmas origens, null quando nenhum nível tem modelo). Existe para a aba Configurações ler 20 bindings resolvidos com uma requisição em vez de 20 — o rate limit é por USUÁRIO. As chaves são separadas por vírgula, deduplicadas, e chave malformada ou mais de 64 chaves no total é 400, nunca descartada em silêncio.
| projectId required | string |
| agents | string Exemplo: agents=criativo,po,dev-lead Slugs de agente separados por vírgula. |
| areas | string Exemplo: areas=dev,qa,infra Chaves de área separadas por vírgula. |
{- "agents": [
- {
- "key": "dev-lead",
- "binding": {
- "modelId": "01JC4Z0000MODELO00000000001",
- "origin": "project",
- "routingPreference": null,
- "skipped": [
- {
- "scope": "agent",
- "modelId": "01JC4Z0000MODELO00000000001",
- "reason": "unavailable"
}
], - "herdadoDoLead": "secops"
}
}
], - "areas": [
- {
- "key": "dev-lead",
- "binding": {
- "modelId": "01JC4Z0000MODELO00000000001",
- "origin": "project",
- "routingPreference": null,
- "skipped": [
- {
- "scope": "agent",
- "modelId": "01JC4Z0000MODELO00000000001",
- "reason": "unavailable"
}
], - "herdadoDoLead": "secops"
}
}
]
}A categoria local são os do Ollama, que não gastam dinheiro; cloud são os de API paga. Modelo que o workspace não ativou não aparece aqui, mas fica nos custos históricos — para vê-lo use a rota do catálogo. A lista é a do workspace DONO do projeto: o mesmo modelo pode estar ligado num workspace e desligado no vizinho (ADR 0049).
| projectId required | string |
{- "local": {
- "ollama": [ ]
}, - "cloud": {
- "anthropic": [ ],
- "openai": [ ]
}
}É isto que alimenta o medidor de tokens ao vivo da tela de sessão.
| sessionId required | string |
{- "id": "01JC4Z0000ORCAMENTO000000001",
- "projectId": "01JC4Z0000PROJETO0000000001",
- "sessionId": null,
- "limitMicros": 50000000,
- "spentMicros": 12400000,
- "policy": "block",
- "lastThresholdNotified": 70,
- "createdAt": "2026-07-12T08:00:00.000Z",
- "updatedAt": "2026-07-27T15:00:00.000Z"
}Mesma conversão de dólar para micro-USD do orçamento do projeto.
| sessionId required | string |
| limitUsd required | number >= 0 Teto em DÓLARES — o único lugar do contrato que fala em dólares, porque é o que uma pessoa digita. A api o converte para micro-USD, e as respostas voltam em micro-USD. |
| policy required | string Valores: "block" "allow"
|
{- "limitUsd": 50,
- "policy": "block"
}{- "id": "01JC4Z0000ORCAMENTO000000001",
- "projectId": "01JC4Z0000PROJETO0000000001",
- "sessionId": null,
- "limitMicros": 50000000,
- "spentMicros": 12400000,
- "policy": "block",
- "lastThresholdNotified": 70,
- "createdAt": "2026-07-12T08:00:00.000Z",
- "updatedAt": "2026-07-27T15:00:00.000Z"
}Server-Sent Events. Quadros delta levam o texto incremental e done fecha com a contabilidade de tokens e custo. Um quadro metering_failed quer dizer que a RESPOSTA saiu mas o custo não foi contabilizado — a falha aparece em vez de sumir. Se o orçamento estourar com policy=block, o stream leva error e nenhum delta. Esta rota NÃO leva agente: manda o texto sozinho ao modelo vinculado, sem histórico e sem prompt de sistema. Numa sessão consultiva onde nenhum agente foi ativado ela é recusada antes de qualquer efeito (RN-682) — converse com um agente por .../agents/:agent/message.
| projectId required | string |
| sessionId required | string |
| text required | string A mensagem do usuário. A resposta volta como stream SSE. |
{- "text": "Summarize what we have already decided about checkout."
}Devolve o binding RESOLVIDO pela cascata, não o binding cru da sessão: sessão sem binding próprio usa o do projeto, e responder null aqui seria a resposta errada para a pergunta certa. O campo origin diz de qual escopo veio o valor.
agentId é opcional e é o agente REALMENTE ativo na sessão agora (o mesmo que RunLlmTurnUseCase usa para rodar o turno) — sem ele, a cascata só enxerga sessão→projeto→workspace (mais o fallback fixo ao Criativo) e nunca reflete PO/Arquiteto/Dev Lead/área depois de um handoff.
| projectId required | string |
| sessionId required | string |
| agentId | string Agente ativo agora nesta sessão (ex.: "po", "arquiteto", "dev-lead"). |
{- "modelId": "01JC4Z0000MODELO00000000001",
- "origin": "project",
- "routingPreference": null,
- "skipped": [
- {
- "scope": "agent",
- "modelId": "01JC4Z0000MODELO00000000001",
- "reason": "unavailable"
}
], - "herdadoDoLead": "secops"
}Vence todo o resto da cascata enquanto a sessão viver.
| projectId required | string |
| sessionId required | string |
| modelId required | string <uuid> Modelo a fixar neste escopo. Tem de existir em |
| routingPreference | string or null Valores: "price" "throughput" "latency" Como um HUB escolhe o upstream que serve o modelo (ADR 0166, RN-583). AUSENTE mantém o valor gravado quando o provider do modelo novo o aceita, e o limpa caso contrário; |
{- "modelId": "9b1c2d3e-4f50-4a61-8b72-0c3d4e5f6a7b",
- "routingPreference": "throughput"
}{- "id": "01JC4Z0000BINDING00000000001",
- "scope": "project",
- "scopeId": "01JC4Z0000PROJETO0000000001",
- "modelId": "01JC4Z0000MODELO00000000001",
- "routingPreference": null,
- "createdBy": "01JC4Z0000USUARIO0000000001",
- "createdAt": "2026-07-11T09:00:00.000Z",
- "updatedAt": "2026-07-11T09:00:00.000Z"
}O dado sempre esteve em token_usage, mas sem agregação nem rota o painel do time não tinha como mostrar o custo por card. Mesmo papel exigido pelo orçamento da sessão.
| sessionId required | string |
[- {
- "actorId": "dev-api",
- "costMicros": 3400000,
- "inputTokens": 128000,
- "outputTokens": 14200,
- "lastCall": {
- "inputTokens": 42000,
- "modelName": "qwen2.5-coder:7b",
- "contextWindow": 128000
}
}
]O público do MEMBRO. Só as linhas do próprio chamador — o ator vem do token autenticado e não há parâmetro para trocá-lo. Em tokens e custo ESTIMADO, sem quebrar por provider nem por credencial: a chave que rodou é a do owner (RN-058) e a fatura dela é dele (RN-060). O eixo de provider passou a existir no relatório do owner (ADR 0076) e continua inalcançável daqui — não há parâmetro de dimensão nesta rota, e o tipo do repositório recusa a combinação (RN-187). Agente não conta: token_usage registra quem gastou, não quem mandou gastar.
| projectId required | string |
| dias | string Janela deslizante. Padrão 30, máximo 180. |
{- "projectId": "9b1c2d3e-4f50-4a61-8b72-0c3d4e5f6a7b",
- "dias": 30,
- "actorId": "9b1c2d3e-4f50-4a61-8b72-0c3d4e5f6a7b",
- "totalMicros": 1250000,
- "assinaturaMicros": 0,
- "inputTokens": 120000,
- "outputTokens": 35000,
- "chamadas": 42,
- "porSessao": [
- {
- "chave": "anthropic/claude-sonnet-4",
- "rotulo": "Loja",
- "actorKind": "agent",
- "costMicros": 1250000,
- "assinaturaMicros": 0,
- "inputTokens": 120000,
- "outputTokens": 35000,
- "chamadas": 42
}
], - "porDia": [
- {
- "dia": "2026-08-09",
- "costMicros": 1250000,
- "chamadas": 42
}
]
}The MEMBER's view of the PROJECT (RN-953, ADR 0233, revising 0063): total and cost per agent/area, NEVER provider or credential — the route has no dimension parameter and never asks for provider.
| projectId required | string |
| dias | string Janela deslizante. Padrão 30, máximo 180. |
{- "projectId": "5a8591dd-4039-49df-9202-96385ba3eff8",
- "dias": 30,
- "totalMicros": 1250000,
- "assinaturaMicros": 0,
- "inputTokens": 120000,
- "outputTokens": 35000,
- "chamadas": 42,
- "porAgente": [
- {
- "chave": "anthropic/claude-sonnet-4",
- "rotulo": "Loja",
- "actorKind": "agent",
- "costMicros": 1250000,
- "assinaturaMicros": 0,
- "inputTokens": 120000,
- "outputTokens": 35000,
- "chamadas": 42
}
], - "porArea": [
- {
- "chave": "anthropic/claude-sonnet-4",
- "rotulo": "Loja",
- "actorKind": "agent",
- "costMicros": 1250000,
- "assinaturaMicros": 0,
- "inputTokens": 120000,
- "outputTokens": 35000,
- "chamadas": 42
}
]
}Existe porque a chave que os agentes gastam é a do DONO do workspace (RN-058) — quem paga a fatura precisa ver a fatura. Agrupa por provider porque essa é a unidade da credencial, e separa o que saiu por AGENTE do que saiu por uma pessoa no chat: os dois saem da mesma chave e respondem perguntas diferentes. Não devolve segredo nenhum.
| workspaceId required | string |
| meses required | string |
{- "workspaceId": "9b1c2d3e-4f50-4a61-8b72-0c3d4e5f6a7b",
- "ownerId": "9b1c2d3e-4f50-4a61-8b72-0c3d4e5f6a7b",
- "meses": 6,
- "totalMicros": 1250000,
- "porProvider": [
- {
- "provider": "openrouter",
- "temCredencial": true,
- "costMicros": 1250000,
- "inputTokens": 120000,
- "outputTokens": 35000,
- "chamadas": 42,
- "costMicrosAgentes": 900000,
- "costMicrosPessoas": 350000,
- "porMes": [
- {
- "mes": "2026-08-01T00:00:00.000Z",
- "costMicros": 1250000,
- "chamadas": 42
}
]
}
]
}Filtra por padrão só publishers OFICIAIS (o allowlist em domain/llm/huggingface-official-publishers.ts) — o mesmo espírito de "curadoria manual sempre" do catálogo de LLM (ADR 0042), aplicado ao selo de "oficial" em vez da ativação. includeCommunity=true traz todo publisher, cada um marcado official: false para a tela desenhar o aviso de segurança — nunca escondido.
| q required | string Texto de busca. |
| includeCommunity | string Passe |
[- {
- "repoId": "meta-llama/Llama-3.1-8B-Instruct-GGUF",
- "publisher": "meta-llama",
- "downloads": 182034,
- "likes": 210,
- "official": true
}
]Primeiro de dois passos explícitos: esta chamada só registra a intenção, em pending_confirmation. Nada é baixado até uma chamada separada a POST .../pull-requests/:id/confirm — o produto nunca roda um pull automático e silencioso.
| workspaceId required | string |
| repoId required | string
|
| estimatedSizeBytes | number Vem da busca quando o Hub publicou o tamanho — nunca estimado por palpite no cliente. |
{- "repoId": "meta-llama/Llama-3.1-8B-Instruct-GGUF",
- "estimatedSizeBytes": 4900000000
}{- "id": "9b1c2d3e-4f50-4a61-8b72-0c3d4e5f6a7b",
- "workspaceId": "9b1c2d3e-4f50-4a61-8b72-0c3d4e5f6a7b",
- "requestedBy": "9b1c2d3e-4f50-4a61-8b72-0c3d4e5f6a7b",
- "repoId": "meta-llama/Llama-3.1-8B-Instruct-GGUF",
- "estimatedSizeBytes": null,
- "status": "pending_confirmation",
- "confirmedAt": null,
- "failedReason": null,
- "createdAt": "2026-08-26T12:00:00.000Z",
- "updatedAt": "2026-08-26T12:00:00.000Z"
}| workspaceId required | string |
| id required | string |
{- "id": "9b1c2d3e-4f50-4a61-8b72-0c3d4e5f6a7b",
- "workspaceId": "9b1c2d3e-4f50-4a61-8b72-0c3d4e5f6a7b",
- "requestedBy": "9b1c2d3e-4f50-4a61-8b72-0c3d4e5f6a7b",
- "repoId": "meta-llama/Llama-3.1-8B-Instruct-GGUF",
- "estimatedSizeBytes": null,
- "status": "pending_confirmation",
- "confirmedAt": null,
- "failedReason": null,
- "createdAt": "2026-08-26T12:00:00.000Z",
- "updatedAt": "2026-08-26T12:00:00.000Z"
}Move pending_confirmation → confirmed → pulling, chama o daemon do Ollama e, em caso de sucesso, faz upsert do modelo no catálogo (models) e o ativa para ESTE workspace. Em caso de falha o pedido termina em failed com um motivo que declara a origem (infra | modelo | código | política) — nunca falha calada. Esta chamada roda de forma SÍNCRONA durante o pull inteiro (ver o caso de uso para o porquê); use a rota GET abaixo para consultar em vez de segurar esta conexão aberta.
| workspaceId required | string |
| id required | string |
{- "id": "9b1c2d3e-4f50-4a61-8b72-0c3d4e5f6a7b",
- "workspaceId": "9b1c2d3e-4f50-4a61-8b72-0c3d4e5f6a7b",
- "requestedBy": "9b1c2d3e-4f50-4a61-8b72-0c3d4e5f6a7b",
- "repoId": "meta-llama/Llama-3.1-8B-Instruct-GGUF",
- "estimatedSizeBytes": null,
- "status": "pending_confirmation",
- "confirmedAt": null,
- "failedReason": null,
- "createdAt": "2026-08-26T12:00:00.000Z",
- "updatedAt": "2026-08-26T12:00:00.000Z"
}The owner of record (workspaces.created_by) is whose key the agents spend (RN-058). Only provider NAMES — never a secret, id or date. ollama (local, no key) never appears.
| workspaceId required | string |
{- "providers": [
- "anthropic",
- "openrouter"
]
}O binding CRU deste escopo. É a raiz da cascata.
| workspaceId required | string |
{- "id": "01JC4Z0000BINDING00000000001",
- "scope": "project",
- "scopeId": "01JC4Z0000PROJETO0000000001",
- "modelId": "01JC4Z0000MODELO00000000001",
- "routingPreference": null,
- "createdBy": "01JC4Z0000USUARIO0000000001",
- "createdAt": "2026-07-11T09:00:00.000Z",
- "updatedAt": "2026-07-11T09:00:00.000Z"
}Vale para todo projeto que não tem binding próprio.
| workspaceId required | string |
| modelId required | string <uuid> Modelo a fixar neste escopo. Tem de existir em |
| routingPreference | string or null Valores: "price" "throughput" "latency" Como um HUB escolhe o upstream que serve o modelo (ADR 0166, RN-583). AUSENTE mantém o valor gravado quando o provider do modelo novo o aceita, e o limpa caso contrário; |
{- "modelId": "9b1c2d3e-4f50-4a61-8b72-0c3d4e5f6a7b",
- "routingPreference": "throughput"
}{- "id": "01JC4Z0000BINDING00000000001",
- "scope": "project",
- "scopeId": "01JC4Z0000PROJETO0000000001",
- "modelId": "01JC4Z0000MODELO00000000001",
- "routingPreference": null,
- "createdBy": "01JC4Z0000USUARIO0000000001",
- "createdAt": "2026-07-11T09:00:00.000Z",
- "updatedAt": "2026-07-11T09:00:00.000Z"
}Same curation level as the pricing route. subscription makes the spend report show the cost apart from the US$ paid (RN-954). Never re-prices the past.
| modelId required | string |
| billing required | string Valores: "api" "subscription" "free" |
{- "billing": "subscription"
}{- "id": "01JC4Z0000MODELO00000000001",
- "provider": "anthropic",
- "name": "claude-opus-4-8",
- "displayName": "Claude Opus 4.8",
- "inputPricePerMillionMicros": 15000000,
- "outputPricePerMillionMicros": 75000000,
- "contextWindow": 200000,
- "supportsToolCalling": true,
- "supportsStreaming": true,
- "supportsVision": false,
- "supportsReasoning": true,
- "generatesImage": false,
- "reasoningBudget": 2048,
- "manualPricing": true,
- "availability": "available",
- "billing": "api",
- "lastSeenAt": "2026-07-30T03:00:00.000Z",
- "createdAt": "2026-07-01T00:00:00.000Z",
- "updatedAt": "2026-07-01T00:00:00.000Z"
}A mais recente primeiro. É um log imutável: nunca recebe UPDATE, e a linha traz o par antes/depois para que a auditoria não dependa de reconstruir o "antes" a partir da linha anterior.
| modelId required | string |
[- {
- "id": "01JC4Z0000PRECO000000000001",
- "modelId": "01JC4Z0000MODELO00000000001",
- "inputBeforeMicros": 2500000,
- "inputAfterMicros": 3000000,
- "outputBeforeMicros": 10000000,
- "outputAfterMicros": 12000000,
- "source": "manual",
- "changedBy": "01JC4Z0000USUARIO0000000001",
- "createdAt": "2026-08-01T12:00:00.000Z"
}
]Vale daqui em diante e NUNCA reprecifica o passado: cada linha de token_usage guarda o preço que produziu o custo dela. A mudança fica registrada com o par antes/depois — consultável na rota de histórico.
| modelId required | string |
| inputPricePerMillionMicros required | number Preço por milhão de tokens de ENTRADA, em micro-USD. |
| outputPricePerMillionMicros required | number Preço por milhão de tokens de SAÍDA, em micro-USD. |
{- "inputPricePerMillionMicros": 2500000,
- "outputPricePerMillionMicros": 10000000
}{- "id": "01JC4Z0000MODELO00000000001",
- "provider": "anthropic",
- "name": "claude-opus-4-8",
- "displayName": "Claude Opus 4.8",
- "inputPricePerMillionMicros": 15000000,
- "outputPricePerMillionMicros": 75000000,
- "contextWindow": 200000,
- "supportsToolCalling": true,
- "supportsStreaming": true,
- "supportsVision": false,
- "supportsReasoning": true,
- "generatesImage": false,
- "reasoningBudget": 2048,
- "manualPricing": true,
- "availability": "available",
- "billing": "api",
- "lastSeenAt": "2026-07-30T03:00:00.000Z",
- "createdAt": "2026-07-01T00:00:00.000Z",
- "updatedAt": "2026-07-01T00:00:00.000Z"
}Curadoria do owner, VALENDO SÓ PARA ESTE WORKSPACE (ADR 0049). Só toca isActive — availability é o que o sync observou no provider, é global, e não muda aqui. Lote inteiro ou nada: id inexistente reprova a chamada sem aplicar linha nenhuma.
| workspaceId required | string |
| modelIds required | Lista de strings <uuid> [ items <uuid > ] Modelos a ligar ou desligar. Lote inteiro ou nada. |
| isActive required | boolean
|
{- "modelIds": [
- "9b1c2d3e-4f50-4a61-8b72-0c3d4e5f6a7b"
], - "isActive": true
}[- {
- "id": "01JC4Z0000MODELO00000000001",
- "provider": "anthropic",
- "name": "claude-opus-4-8",
- "displayName": "Claude Opus 4.8",
- "inputPricePerMillionMicros": 15000000,
- "outputPricePerMillionMicros": 75000000,
- "contextWindow": 200000,
- "supportsToolCalling": true,
- "supportsStreaming": true,
- "supportsVision": false,
- "supportsReasoning": true,
- "generatesImage": false,
- "reasoningBudget": 2048,
- "manualPricing": true,
- "availability": "available",
- "billing": "api",
- "lastSeenAt": "2026-07-30T03:00:00.000Z",
- "createdAt": "2026-07-01T00:00:00.000Z",
- "updatedAt": "2026-07-01T00:00:00.000Z",
- "isActive": true,
- "uses": [
- "codigo"
], - "freeRoutingAlias": false
}
]A tela de curadoria. Modelo descoberto pelo sync entra como isActive: false e só aparece aqui até alguém ativá-lo; modelo que sumiu do provider vem com availability: "unavailable" e nunca é apagado, porque bindings e histórico de custo apontam para ele. isActive é PARA ESTE workspace (ADR 0049); availability é global, porque é o que o sync observou no provider.
| workspaceId required | string |
{- "local": {
- "ollama": [ ]
}, - "cloud": {
- "anthropic": [ ],
- "openai": [ ]
}
}Mesmo caso de uso da rota interna que o engine agenda — o botão de atualizar da tela de curadoria não tem caminho próprio, então nunca há duas reconciliações que possam divergir.
{- "porProvider": [
- {
- "provider": "openai",
- "descobertos": 3,
- "reencontrados": 1,
- "indisponibilizados": 0,
- "pulado": "sem_capability",
- "origemDaFalha": "infra",
- "detalhe": "openai responded with status 401"
}
]
}O eixo que nenhum catálogo publica: "bom para código" não é capability declarada por provider nenhum, é opinião do operador — e vale SÓ para este workspace (ADR 0049). Eixo independente da ativação: marcar um uso não liga o modelo no seletor, e mudar o uso não desliga o que estava ligado. A lista de usos SUBSTITUI a anterior.
| workspaceId required | string |
| modelIds required | Lista de strings <uuid> [ items <uuid > ] Modelos a marcar. Lote inteiro ou nada. |
| uses required | Lista de strings Itens Valores: "codigo" "documentacao" "analise" "imagem" "conversa" The COMPLETE list of uses — replaces the previous one, does not add to it. An empty list unmarks all. A closed vocabulary: a use outside it fails the call, because free text would give |
{- "modelIds": [
- "9b1c2d3e-4f50-4a61-8b72-0c3d4e5f6a7b"
], - "uses": [
- "codigo",
- "analise"
]
}[- {
- "id": "01JC4Z0000MODELO00000000001",
- "provider": "anthropic",
- "name": "claude-opus-4-8",
- "displayName": "Claude Opus 4.8",
- "inputPricePerMillionMicros": 15000000,
- "outputPricePerMillionMicros": 75000000,
- "contextWindow": 200000,
- "supportsToolCalling": true,
- "supportsStreaming": true,
- "supportsVision": false,
- "supportsReasoning": true,
- "generatesImage": false,
- "reasoningBudget": 2048,
- "manualPricing": true,
- "availability": "available",
- "billing": "api",
- "lastSeenAt": "2026-07-30T03:00:00.000Z",
- "createdAt": "2026-07-01T00:00:00.000Z",
- "updatedAt": "2026-07-01T00:00:00.000Z",
- "isActive": true,
- "uses": [
- "codigo"
], - "freeRoutingAlias": false
}
]O público do OWNER. O eixo de PROVIDER voltou (ADR 0076, RN-186) e está aqui, não na rota do membro, porque quebrar por provider é quebrar por CREDENCIAL — e credencial é assunto de quem paga (RN-060). Complementa credential-spend, que continua respondendo a pergunta da FATURA (por mês, com o vínculo à chave que existe hoje). Pessoa e agente também vêm em blocos separados, por actor_kind. Janela deslizante em dias, e a série diária vem DENSA — dia sem gasto entra como zero, senão o sparkline mente sobre o ritmo.
| workspaceId required | string |
| dias | string Janela deslizante. Padrão 30, máximo 180. |
| projectId | string Slice to ONE project of this workspace (RN-952). A project from another workspace answers 404. |
{- "workspaceId": "9b1c2d3e-4f50-4a61-8b72-0c3d4e5f6a7b",
- "ownerId": "9b1c2d3e-4f50-4a61-8b72-0c3d4e5f6a7b",
- "dias": 30,
- "projectId": { },
- "totalMicros": 1250000,
- "assinaturaMicros": 0,
- "inputTokens": 120000,
- "outputTokens": 35000,
- "chamadas": 42,
- "porModelo": [
- {
- "chave": "anthropic/claude-sonnet-4",
- "rotulo": "Loja",
- "actorKind": "agent",
- "costMicros": 1250000,
- "assinaturaMicros": 0,
- "inputTokens": 120000,
- "outputTokens": 35000,
- "chamadas": 42
}
], - "porProvider": [
- {
- "chave": "anthropic/claude-sonnet-4",
- "rotulo": "Loja",
- "actorKind": "agent",
- "costMicros": 1250000,
- "assinaturaMicros": 0,
- "inputTokens": 120000,
- "outputTokens": 35000,
- "chamadas": 42
}
], - "porProjeto": [
- {
- "chave": "anthropic/claude-sonnet-4",
- "rotulo": "Loja",
- "actorKind": "agent",
- "costMicros": 1250000,
- "assinaturaMicros": 0,
- "inputTokens": 120000,
- "outputTokens": 35000,
- "chamadas": 42
}
], - "porSessao": [
- {
- "chave": "anthropic/claude-sonnet-4",
- "rotulo": "Loja",
- "actorKind": "agent",
- "costMicros": 1250000,
- "assinaturaMicros": 0,
- "inputTokens": 120000,
- "outputTokens": 35000,
- "chamadas": 42
}
], - "porAtor": [
- {
- "chave": "anthropic/claude-sonnet-4",
- "rotulo": "Loja",
- "actorKind": "agent",
- "costMicros": 1250000,
- "assinaturaMicros": 0,
- "inputTokens": 120000,
- "outputTokens": 35000,
- "chamadas": 42
}
], - "porOwner": [
- {
- "chave": "anthropic/claude-sonnet-4",
- "rotulo": "Loja",
- "actorKind": "agent",
- "costMicros": 1250000,
- "assinaturaMicros": 0,
- "inputTokens": 120000,
- "outputTokens": 35000,
- "chamadas": 42
}
], - "porAgente": [
- {
- "chave": "anthropic/claude-sonnet-4",
- "rotulo": "Loja",
- "actorKind": "agent",
- "costMicros": 1250000,
- "assinaturaMicros": 0,
- "inputTokens": 120000,
- "outputTokens": 35000,
- "chamadas": 42
}
], - "porDia": [
- {
- "dia": "2026-08-09",
- "costMicros": 1250000,
- "chamadas": 42
}
]
}The non-owner's global page (RN-953). Only the caller's rows, by PROJECT and by day, without provider or credential.
| workspaceId required | string |
| dias | string Janela deslizante. Padrão 30, máximo 180. |
{- "workspaceId": "ef0efa32-d1c1-43d4-a5e2-fe7b4f00403c",
- "dias": 30,
- "actorId": "d2f1b55c-8cee-4a0d-af57-aebaeff77518",
- "totalMicros": 1250000,
- "assinaturaMicros": 0,
- "inputTokens": 120000,
- "outputTokens": 35000,
- "chamadas": 42,
- "porProjeto": [
- {
- "chave": "anthropic/claude-sonnet-4",
- "rotulo": "Loja",
- "actorKind": "agent",
- "costMicros": 1250000,
- "assinaturaMicros": 0,
- "inputTokens": 120000,
- "outputTokens": 35000,
- "chamadas": 42
}
], - "porDia": [
- {
- "dia": "2026-08-09",
- "costMicros": 1250000,
- "chamadas": 42
}
]
}{- "id": "01JC4Z0000PROJETO0000000001",
- "workspaceId": "01JC4Z0000WORKSPACE00000001",
- "name": "Checkout",
- "slug": "checkout",
- "workspaceDirName": "checkout-3f2b1c8e",
- "executionMode": "container",
- "workspacePath": null,
- "workspaceVerifiedAt": null,
- "mirrorPath": null,
- "language": "pt-BR",
- "createdBy": "01JC4Z0000USUARIO0000000001",
- "taskBudgetMicros": 500000,
- "maxConsecutiveBlocked": 3,
- "storyPromotion": "auto",
- "createdAt": "2026-07-21T11:00:00.000Z",
- "updatedAt": "2026-07-21T11:00:00.000Z"
}| projectId required | string |
| name | string >= 2 characters |
| slug | string^[a-z0-9]+(-[a-z0-9]+)*$ kebab-case. Único dentro do workspace. |
| maxConsecutiveBlocked | number Circuit breaker (Fase 12b — RN-047): quantas tarefas consecutivas terminando bloqueadas param o dev agent do módulo em idle_tripped. Omitido usa o padrão do domínio (DEFAULT_MAX_CONSECUTIVE_BLOCKED). Vale da PRÓXIMA ativação de execução em diante — não afeta agentes já rodando. |
| storyPromotion | string Valores: "manual" "auto" Quem promove uma história de |
| language | string <= 35 characters O idioma do PROJETO (RN-619) — artefatos compartilhados e turnos sem autor humano o usam. Qualquer código BCP-47 que o servidor reconhece, gravado na forma canônica; um não reconhecido é 400. Omitido, o projeto começa com o idioma de resposta efetivo de quem o criou (RN-618: escolha da conta > detecção confirmada > idioma da interface). |
{- "name": "Checkout",
- "slug": "checkout",
- "maxConsecutiveBlocked": 3,
- "storyPromotion": "auto",
- "language": "pt-BR"
}{- "id": "01JC4Z0000PROJETO0000000001",
- "workspaceId": "01JC4Z0000WORKSPACE00000001",
- "name": "Checkout",
- "slug": "checkout",
- "workspaceDirName": "checkout-3f2b1c8e",
- "executionMode": "container",
- "workspacePath": null,
- "workspaceVerifiedAt": null,
- "mirrorPath": null,
- "language": "pt-BR",
- "createdBy": "01JC4Z0000USUARIO0000000001",
- "taskBudgetMicros": 500000,
- "maxConsecutiveBlocked": 3,
- "storyPromotion": "auto",
- "createdAt": "2026-07-21T11:00:00.000Z",
- "updatedAt": "2026-07-21T11:00:00.000Z"
}Leva junto as sessões, o event log e as ações propostas.
| projectId required | string |
{- "id": "01JC4Z0000PROJETO0000000001",
- "workspaceId": "01JC4Z0000WORKSPACE00000001",
- "name": "Checkout",
- "slug": "checkout",
- "workspaceDirName": "checkout-3f2b1c8e",
- "executionMode": "container",
- "workspacePath": null,
- "workspaceVerifiedAt": null,
- "mirrorPath": null,
- "language": "pt-BR",
- "createdBy": "01JC4Z0000USUARIO0000000001",
- "taskBudgetMicros": 500000,
- "maxConsecutiveBlocked": 3,
- "storyPromotion": "auto",
- "createdAt": "2026-07-21T11:00:00.000Z",
- "updatedAt": "2026-07-21T11:00:00.000Z"
}Enquanto o status é sem_decisao, o container do projeto não sobe e a aba Código responde 409 — esse é o portão da RN-105. Ver o estado exige a mesma permissão de ver o projeto, então viewer.
| projectId required | string |
{- "status": "sem_decisao",
- "decisao": {
- "image": "node:22-bookworm-slim",
- "rationale": "The module_map is all TypeScript over Node; the slim variant is enough and reduces surface.",
- "network": "none",
- "resources": {
- "cpus": 2,
- "memoryMb": 4096,
- "pidsLimit": 512
}
}, - "version": 0,
- "eventId": "01JC4Z0000EVENTO000000001",
- "decidedAt": { }
}null quando o projeto nunca foi provisionado — o caso comum hoje, porque nenhum orquestrador real transiciona esta tabela ainda (RN-243/RN-267). status/imageVersion/resources são o que foi REGISTRADO; observado é o que o daemon do Docker relata agora, perguntado pelo broker (ADR 0130). Os dois nunca se fundem: um container morto por fora aparece como running registrado e exited observado. Quando não houve como olhar, observado é null e naoObservado diz por quê — ele nunca herda o estado registrado (RN-468).
| projectId required | string |
{- "status": "provisioning",
- "imageVersion": 1,
- "resources": {
- "cpus": 2,
- "memoryMb": 4096,
- "pidsLimit": 512
}, - "failureReason": null,
- "createdAt": "2019-08-24T14:15:22Z",
- "statusChangedAt": "2019-08-24T14:15:22Z",
- "observado": {
- "containerId": "c0ffeebabe...",
- "nome": "brabo-exp002-f52be111",
- "estado": "running",
- "imagem": "node:22-bookworm-slim",
- "iniciadoEm": { }
}, - "naoObservado": "broker-nao-configurado",
- "detalheDaObservacao": null
}The current declaration (ports on 127.0.0.1, DNS, extra hosts), or null. It is what was DECLARED; it only takes effect when the container is recreated (container_recreate).
| projectId required | string |
{- "ports": [
- {
- "host": 8080,
- "container": 3000,
- "protocol": "tcp"
}
], - "dns": [
- "string"
], - "extraHosts": [
- {
- "name": "api.local",
- "ip": "10.0.0.5"
}
], - "version": 1
}Emits a new version of artifact.container_network (the current one replaces the previous). Not an external effect: nothing changes in Docker until a container_recreate is approved (AT-608, RN-932). 400 rede_invalida names the field.
| projectId required | string |
Lista de objects (PortaDto) Up to 10 TCP ports published on 127.0.0.1 only. | |
| dns | Lista de strings Up to 3 literal IPs. |
Lista de objects (HostExtraDto) Up to 10 |
{- "ports": [
- {
- "host": 8080,
- "container": 3000,
- "protocol": "tcp"
}
], - "dns": [
- "string"
], - "extraHosts": [
- {
- "name": "api.local",
- "ip": "10.0.0.5"
}
]
}{- "ports": [
- {
- "host": 8080,
- "container": 3000,
- "protocol": "tcp"
}
], - "dns": [
- "string"
], - "extraHosts": [
- {
- "name": "api.local",
- "ip": "10.0.0.5"
}
], - "version": 1
}Migra execution_mode/workspacePath de um projeto que JÁ existe (RN-447..450, ADR 0111) — rota dedicada, separada do PATCH, porque orquestra mais do que gravar uma coluna: move o permissions.json para a raiz de escopo nova, aposenta a linha de ciclo de vida do container ao sair do modo container, e recusa (409) enquanto qualquer dev agent do projeto não estiver ocioso, já que um agente rodando não re-resolve sozinho onde está o worktree dele.
| projectId required | string |
| executionMode required | string Valores: "container" "mounted" "runner" O novo modo de execução. Pode repetir o modo atual do projeto — nesse caso, só |
| workspacePath | string O caminho absoluto da pasta do usuário — obrigatório quando |
{- "executionMode": "runner",
- "workspacePath": "/home/you/projects/store"
}{- "id": "01JC4Z0000PROJETO0000000001",
- "workspaceId": "01JC4Z0000WORKSPACE00000001",
- "name": "Checkout",
- "slug": "checkout",
- "workspaceDirName": "checkout-3f2b1c8e",
- "executionMode": "container",
- "workspacePath": null,
- "workspaceVerifiedAt": null,
- "mirrorPath": null,
- "language": "pt-BR",
- "createdBy": "01JC4Z0000USUARIO0000000001",
- "taskBudgetMicros": 500000,
- "maxConsecutiveBlocked": 3,
- "storyPromotion": "auto",
- "createdAt": "2026-07-21T11:00:00.000Z",
- "updatedAt": "2026-07-21T11:00:00.000Z"
}Só quem foi associado AQUI, com o papel dessa associação — quem alcança o projeto só pelo workspace não tem linha e não aparece.
| projectId required | string |
[- {
- "userId": "01JC4Z0000USUARIO0000000002",
- "role": "maintainer",
- "name": "Senior Dev",
- "email": "dev@brabo.dev"
}
]Este papel SOBREPÕE o que a pessoa tem no workspace, nos dois sentidos: associar alguém como viewer aqui restringe de fato um developer do workspace neste projeto. Dois movimentos são recusados com 403 e não podem ser habilitados em lugar nenhum: rebaixar um owner do workspace, e mudar o PRÓPRIO papel — para baixo ou para cima, já que a autopromoção é a metade que escala privilégio.
| projectId required | string |
| userId required | string <uuid> Id do usuário a associar. |
| role required | string Valores: "viewer" "developer" "maintainer" "owner" Papel nesta associação. Num PROJETO ele SOBREPÕE o papel de workspace nos dois sentidos — o papel efetivo é este sempre que a associação existe, maior OU menor — e dois movimentos são recusados com 403: rebaixar um |
{- "userId": "3f1b2c8e-5a4d-4b7e-9c10-2d6f8a1b4c33",
- "role": "developer"
}{- "projectId": "01JC4Z0000PROJETO0000000001",
- "userId": "01JC4Z0000USUARIO0000000002",
- "role": "maintainer",
- "createdAt": "2026-07-22T08:05:00.000Z"
}Remove só a associação de PROJETO. Quem tem papel no workspace continua vendo o projeto por herança — e como o papel de projeto SOBREPÕE o de workspace nos dois sentidos, essa herança pode ser MENOR que o que a linha concedia. Remover a própria linha é, por isso, recusado com 403 sempre que o efeito líquido é rebaixamento (inclusive quando não há papel de workspace nenhum); removê-la quando o workspace segura o mesmo papel, e remover qualquer outra pessoa, continuam funcionando.
| projectId required | string |
| userId required | string |
O caminho absoluto, NA MÁQUINA DO USUÁRIO, para onde o agente local copia o trabalho (RN-515, ADR 0147) — uma pasta FORA da base montada, que é toda a razão de o espelho existir. Por projeto e nunca global: um destino global aterrissaria os artefatos do projeto B na pasta do projeto A, e o usuário descobriria pelo conteúdo, não por um erro. mirrorPath: null o LIMPA e desliga o espelho — a chave é obrigatória, omiti-la é 400, porque um corpo que omite o campo seria indistinguível de pedir para limpar. null é o estado NORMAL de um projeto. maintainer, o mesmo mínimo de execution-mode e projects-base: esta rota fala de um caminho no próprio sistema de arquivos do operador. Valida SÓ a forma LÉXICA e recusa, com 400, destino dentro de workspacePath ou que o contenha (os dois sentidos do mesmo laço), e qualquer destino num projeto container. Nunca toca o disco: a API não enxerga a máquina onde o destino vai morar. Gravar o espelho NÃO é proposed_action — é configuração que o usuário declarou, não agente pedindo para agir.
| projectId required | string |
| mirrorPath required | string or null O caminho absoluto, NA MÁQUINA DO USUÁRIO, para onde o agente local copia o trabalho do projeto (RN-515, ADR 0147). Mande |
{- "mirrorPath": "/home/you/mirrors/store"
}{- "id": "01JC4Z0000PROJETO0000000001",
- "workspaceId": "01JC4Z0000WORKSPACE00000001",
- "name": "Checkout",
- "slug": "checkout",
- "workspaceDirName": "checkout-3f2b1c8e",
- "executionMode": "container",
- "workspacePath": null,
- "workspaceVerifiedAt": null,
- "mirrorPath": null,
- "language": "pt-BR",
- "createdBy": "01JC4Z0000USUARIO0000000001",
- "taskBudgetMicros": 500000,
- "maxConsecutiveBlocked": 3,
- "storyPromotion": "auto",
- "createdAt": "2026-07-21T11:00:00.000Z",
- "updatedAt": "2026-07-21T11:00:00.000Z"
}Telemetria que o agente local empurrou pelo canal depois de copiar o trabalho para a pasta do usuário (ADR 0147, ponto 7) — nunca o event log, porque rodada de espelho não tem sessão e session_events.session_id é NOT NULL (o mesmo raciocínio que fez de rag_searches uma tabela). TRÊS respostas que nunca colapsam numa só (RN-088): never (nenhuma rodada relatada), synced (a última rodada copiou — filesCopied pode ser 0, que quer dizer "olhou e não havia nada a copiar") e failed. Uma falha nunca apaga a última sincronização bem-sucedida, e um sucesso nunca apaga o último erro: qual é o CORRENTE sai da comparação dos dois carimbos, então a tela pode dizer "falhando desde hoje, a última cópia boa foi ontem com 412 arquivos". lastDestination é CONGELADO — diverge de mirrorPath depois que alguém troca o destino, porque a concessão viaja no join e só muda quando o runner reconecta (RN-516). viewer, o mesmo mínimo de ler o projeto, que já traz mirrorPath.
| projectId required | string |
{- "mirrorPath": "/home/you/mirrors/store",
- "status": "synced",
- "lastSyncedAt": "2026-09-07T12:04:00.000Z",
- "filesCopied": 412,
- "filesSkipped": 3,
- "filesRefused": 0,
- "lastDestination": "/home/you/mirrors/store",
- "lastError": null,
- "lastErrorAt": null
}É o arquivo físico na raiz do workspace do projeto, não uma coluna do banco.
| projectId required | string |
{- "allow": [
- "Terminal(pnpm test:*)",
- "Terminal(pnpm lint)"
], - "deny": [
- "Terminal(rm -rf *)",
- "Terminal(git push --force*)"
], - "ask": [
- "Terminal(pnpm add *)"
]
}Substitui as três listas de uma vez. deny continua vencendo allow na avaliação — não há como liberar por aqui algo negado.
| projectId required | string |
| allow required | Lista de strings Padrões aprovados automaticamente. |
| deny required | Lista de strings Padrões proibidos. |
| ask required | Lista de strings Padrões que sempre pedem decisão humana. |
{- "allow": [
- "Terminal(pnpm test:*)",
- "Terminal(pnpm lint)"
], - "deny": [
- "Terminal(rm -rf *)",
- "Terminal(git push --force*)"
], - "ask": [
- "Terminal(pnpm add *)"
]
}{- "allow": [
- "Terminal(pnpm test:*)",
- "Terminal(pnpm lint)"
], - "deny": [
- "Terminal(rm -rf *)",
- "Terminal(git push --force*)"
], - "ask": [
- "Terminal(pnpm add *)"
]
}Nunca inclui o token bruto — só o que já foi emitido.
| projectId required | string |
[- {
- "id": "01JC4Z0000PAT000000000001",
- "name": "laptop",
- "projectId": "01JC4Z0000PROJETO000000001",
- "createdAt": "2026-08-22T12:00:00.000Z",
- "expiresAt": null,
- "revokedAt": null,
- "lastUsedAt": null
}
]O token bruto (brb_...) só aparece NESTA resposta — não fica recuperável depois, só o hash é guardado. Use em --token/BRABO_ACCOUNT_TOKEN do brabo-runner.
| projectId required | string |
| name required | string Nome pra você reconhecer este token depois — não é único. |
| expiresInDays | number Expira em N dias. Omitido = sem expiração (revogável a qualquer hora, mas nunca expira sozinho). |
{- "name": "laptop",
- "expiresInDays": 90
}{- "id": "01JC4Z0000PAT000000000001",
- "name": "laptop",
- "projectId": "01JC4Z0000PROJETO000000001",
- "createdAt": "2026-08-22T12:00:00.000Z",
- "expiresAt": null,
- "revokedAt": null,
- "lastUsedAt": null,
- "token": "brb_9f8a..."
}Idempotente — revogar de novo não é erro. Desde a RN-685 (ADR 0201) também DERRUBA as conexões de runner abertas com ESTE token, e só elas — a chave de dispositivo do mesmo usuário fica de pé —, e anula os tickets dele ainda não usados. Engine fora do ar NÃO faz a revogação falhar.
| projectId required | string |
| tokenId required | string |
Resposta a incidente — dev desligado com token vazando (RN-427). Idempotente — revogar de novo não é erro. Desde a RN-685 (ADR 0201) também DERRUBA o runner já conectado com este token, e só ele.
| projectId required | string |
| tokenId required | string |
Visão de maintainer para resposta a incidente (RN-427) — inclui o dono de cada token. Nunca inclui o token bruto.
| projectId required | string |
[- {
- "id": "01JC4Z0000PAT000000000001",
- "name": "laptop",
- "projectId": "01JC4Z0000PROJETO000000001",
- "createdAt": "2026-08-22T12:00:00.000Z",
- "expiresAt": null,
- "revokedAt": null,
- "lastUsedAt": null,
- "userId": "01JC4Z0000USUARIO000000001",
- "userEmail": "dev@exemplo.com"
}
]Ninguém revoga o que não consegue ver (RN-519). Inclui as já REVOGADAS — sumir com a linha faria a tela afirmar que a chave nunca existiu. Nunca devolve a JWK pública, e a privada a api nunca viu. lastUsedAt nulo é o sinal de uma chave ÓRFÃ: registrada e nunca usada por runner nenhum. Desde a RN-543 inclui também as chaves de MÁQUINA do chamador (especie: "maquina", projectId nulo), que servem este projeto sem pertencer a ele — sem elas na lista, uma chave de máquina seria invisível em toda tela.
| projectId required | string |
[- {
- "id": "01JC4Z0000CHAVE000000000001",
- "name": "laptop",
- "projectId": "01JC4Z0000PROJETO000000001",
- "especie": "projeto",
- "createdAt": "2026-08-27T12:00:00.000Z",
- "revokedAt": null,
- "lastUsedAt": null
}
]Idempotente — revogar de novo não é erro. Desde a RN-520 também DERRUBA a conexão viva: antes, revogar só impedia ticket NOVO, e um runner já conectado seguia executando comando aprovado. Desde a RN-685 (ADR 0201) o alvo é a CHAVE: caem só as conexões abertas com ela — em todo projeto, se for de máquina —, e outro runner do mesmo usuário, conectado com PAT ou com outra chave, fica de pé. Os tickets dela ainda não usados são anulados. Engine fora do ar ou nenhum runner conectado NÃO fazem a revogação falhar.
| projectId required | string |
| deviceKeyId required | string |
Autentica connect/3 do socket Phoenix /runner (tópico terminal:<projectId>, papel "runner") — NÃO é o JWT reaproveitado, e esta rota em si só aceita um Personal Access Token no lugar do JWT de sessão (ADR 0105). TTL de 30s e uso único. Recusa com 400 se o projeto não estiver no modo "runner" (RN-421): o runner não tem o que servir num projeto cujo código mora no container gerenciado.
| projectId required | string |
{- "ticket": "string",
- "expiresAt": "2026-08-09T12:00:30.000Z",
- "engineWsUrl": "ws://localhost:4000/runner"
}Mesmo socket /runner, mesmo tópico, papel "terminal" — quem VÊ o terminal (o comando em si só roda de verdade se houver um runner conectado). Vale para qualquer modo de projeto.
| projectId required | string |
{- "ticket": "string",
- "expiresAt": "2026-08-09T12:00:30.000Z",
- "engineWsUrl": "ws://localhost:4000/runner"
}Autenticada por uma chave de dispositivo de MÁQUINA (ADR 0154) — NUNCA por JWT de sessão, e nunca por uma credencial presa a um projeto, que responde 403. Devolve os projetos em execution_mode: "runner" nos quais o dono da chave alcança pelo menos developer (o mesmo mínimo de POST .../runner-ticket), com o nome da pasta e o estado de verificação de cada um. O agente pergunta em vez de varrer o disco: a base da máquina é do usuário e pode ter pasta que não é projeto nenhum.
[- {
- "projectId": "01JC4Z0000PROJETO000000001",
- "name": "Brabo",
- "workspaceDirName": "brabo-01jc4z",
- "workspaceVerifiedAt": null
}
]Uma linha por projeto do workspace — desde a RN-521 um projeto que nunca provisionou container está PRESENTE com registrado: null em vez de ausente, porque esta página é o caminho humano para subir o primeiro. O estado observado só é perguntado ao broker para linhas provisioning/running, e só até um orçamento por carga — ver naoVerificado nas linhas puladas, e o ADR 0136 para o raciocínio.
| workspaceId required | string |
[- {
- "projectId": "01JC4Z0000PROJETO000001",
- "projectName": "exp002",
- "projectSlug": "exp002",
- "executionMode": "runner",
- "registrado": {
- "status": "running",
- "imageVersion": 1,
- "imagem": "node:22-bookworm-slim",
- "resources": {
- "cpus": 2,
- "memoryMb": 4096,
- "pidsLimit": 512
}, - "failureReason": null,
- "createdAt": "2019-08-24T14:15:22Z",
- "statusChangedAt": "2019-08-24T14:15:22Z"
}, - "temImagemDecidida": true,
- "workspaceVerifiedAt": { },
- "observado": {
- "containerId": "c0ffeebabe...",
- "nome": "brabo-exp002-f52be111",
- "estado": "running",
- "imagem": "node:22-bookworm-slim",
- "iniciadoEm": { }
}, - "naoObservado": "broker-nao-configurado",
- "detalheDaObservacao": null,
- "naoVerificado": null,
- "acaoPendente": {
- "id": "01JC4Z8QK3M7YV2N5T9B0PXHRC",
- "projectId": "01JC4Z0000PROJETO0000000001",
- "sessionId": "01JC4Z8QK3M7YV2N5T9B0PXHRA",
- "seq": 7,
- "actionType": "terminal",
- "payload": {
- "command": "pnpm test"
}, - "status": "pending",
- "resolvedPolicy": "require_approval",
- "actor": {
- "kind": "agent",
- "id": "dev-api"
}, - "decidedBy": null,
- "decidedAt": null,
- "rejectionReason": null,
- "executionResult": null,
- "createdAt": "2026-07-27T14:33:10.900Z",
- "updatedAt": "2026-07-27T14:33:10.900Z"
}, - "decisaoVigente": {
- "version": 3,
- "image": "node:22",
- "network": "egress"
}, - "imagemDesatualizada": false,
- "brokerConfigurado": false
}
]status é pending (o padrão) ou failed. actionType filtra por tipo — ex.: git_merge, para a aba de PRs achar a proposta de merge de uma PR sem saber qual sessão a criou. status=failed EXIGE actionType e lista as ações cuja execução falhou — a aba de PRs lê dali o último merge recusado de cada PR (conflito de merge traz executionResult.conflictingFiles).
| projectId required | string |
| status | string Exemplo: status=pending |
| actionType | string Exemplo: actionType=git_merge |
[- {
- "id": "01JC4Z8QK3M7YV2N5T9B0PXHRC",
- "projectId": "01JC4Z0000PROJETO0000000001",
- "sessionId": "01JC4Z8QK3M7YV2N5T9B0PXHRA",
- "seq": 7,
- "actionType": "terminal",
- "payload": {
- "command": "pnpm test"
}, - "status": "pending",
- "resolvedPolicy": "require_approval",
- "actor": {
- "kind": "agent",
- "id": "dev-api"
}, - "decidedBy": null,
- "decidedAt": null,
- "rejectionReason": null,
- "executionResult": null,
- "createdAt": "2026-07-27T14:33:10.900Z",
- "updatedAt": "2026-07-27T14:33:10.900Z"
}
]Só as regras gravadas explicitamente. Agente sem linha aqui cai no padrão do permissions.json do projeto.
| projectId required | string |
[- {
- "agentId": "dev-api",
- "actionType": "terminal",
- "mode": "auto_approve"
}
]Upsert por (agente, tipo). NÃO sobrepõe o permissions.json: padrão já em deny continua bloqueado por mais autonomia que se conceda aqui. actionType: "*" é o "modo automático" (RN-153) — autonomia para QUALQUER tipo de ação deste agente; regra específica gravada depois continua vencendo a curinga para aquele tipo.
| projectId required | string |
| agentId required | string Slug do agente. |
| actionType required | string Valores: "terminal" "git_commit" "git_push" "pr_open" "spend" "git_repo_create" "git_branch_create" "git_branch_protect" "write_file" "open_adr_pr" "git_merge" "open_infra_pr" "instruction_patch" "parallelize" "raise_max_parallel" "propose_execution_plan" "assess_implementability" "container_start" "container_stop" "container_remove" "container_recreate" "container_start_via_runner" "qa_environment_start" "*" Tipo de ação, ou |
| mode required | string Valores: "auto_approve" "require_approval" "deny" A autonomia deste agente para este tipo. NÃO sobrepõe o |
{- "agentId": "dev-api",
- "actionType": "terminal",
- "mode": "auto_approve"
}Ordenadas por seq; use nextCursor como afterSeq. latest=true busca a CAUDA em vez disso (as limit ações mais novas, ainda em ordem crescente) e ignora afterSeq. status=pending mantém só as ações esperando decisão — com latest, as pendentes mais novas, para que uma sessão com mais de 200 ações não empurre uma pendente nova para fora da janela (RN-637).
| projectId required | string |
| sessionId required | string |
| afterSeq | string Exemplo: afterSeq=6 |
| limit | string Exemplo: limit=50 |
| latest | string Exemplo: latest=true Busca a cauda da sessão; ignora |
| status | string Exemplo: status=pending Só |
{- "items": [
- {
- "id": "01JC4Z8QK3M7YV2N5T9B0PXHRC",
- "projectId": "01JC4Z0000PROJETO0000000001",
- "sessionId": "01JC4Z8QK3M7YV2N5T9B0PXHRA",
- "seq": 7,
- "actionType": "terminal",
- "payload": {
- "command": "pnpm test"
}, - "status": "pending",
- "resolvedPolicy": "require_approval",
- "actor": {
- "kind": "agent",
- "id": "dev-api"
}, - "decidedBy": null,
- "decidedAt": null,
- "rejectionReason": null,
- "executionResult": null,
- "createdAt": "2026-07-27T14:33:10.900Z",
- "updatedAt": "2026-07-27T14:33:10.900Z"
}
], - "nextCursor": 7
}A ação é avaliada contra o permissions.json do projeto na criação. O resolvedPolicy da resposta diz o que a política decidiu: auto_approve já sai executável, require_approval espera decisão humana, e deny nasce recusada. Não há caminho que execute sem passar por aqui.
| projectId required | string |
| sessionId required | string |
| actionType required | string Valores: "terminal" "git_commit" "git_push" "pr_open" "spend" "git_repo_create" "git_branch_create" "git_branch_protect" "write_file" "open_adr_pr" "git_merge" "open_infra_pr" "instruction_patch" "parallelize" "raise_max_parallel" "propose_execution_plan" "assess_implementability" "container_start" "container_stop" "container_remove" "container_recreate" "container_start_via_runner" "qa_environment_start" Tipo da ação. Cada tipo tem papel mínimo próprio: |
required | object Quem está propondo. |
required | object Parâmetros da ação, com forma específica do |
{- "actionType": "terminal",
- "actor": {
- "kind": "agent",
- "id": "dev-api"
}, - "payload": {
- "command": "pnpm test"
}
}{- "id": "01JC4Z8QK3M7YV2N5T9B0PXHRC",
- "projectId": "01JC4Z0000PROJETO0000000001",
- "sessionId": "01JC4Z8QK3M7YV2N5T9B0PXHRA",
- "seq": 7,
- "actionType": "terminal",
- "payload": {
- "command": "pnpm test"
}, - "status": "pending",
- "resolvedPolicy": "require_approval",
- "actor": {
- "kind": "agent",
- "id": "dev-api"
}, - "decidedBy": null,
- "decidedAt": null,
- "rejectionReason": null,
- "executionResult": null,
- "createdAt": "2026-07-27T14:33:10.900Z",
- "updatedAt": "2026-07-27T14:33:10.900Z"
}Vale só para esta ação. A decisão fica no event log com o id de quem decidiu.
| projectId required | string |
| sessionId required | string |
| actionId required | string |
{- "id": "01JC4Z8QK3M7YV2N5T9B0PXHRC",
- "projectId": "01JC4Z0000PROJETO0000000001",
- "sessionId": "01JC4Z8QK3M7YV2N5T9B0PXHRA",
- "seq": 7,
- "actionType": "terminal",
- "payload": {
- "command": "pnpm test"
}, - "status": "pending",
- "resolvedPolicy": "require_approval",
- "actor": {
- "kind": "agent",
- "id": "dev-api"
}, - "decidedBy": null,
- "decidedAt": null,
- "rejectionReason": null,
- "executionResult": null,
- "createdAt": "2026-07-27T14:33:10.900Z",
- "updatedAt": "2026-07-27T14:33:10.900Z"
}Além de liberar esta ação, acrescenta o padrão correspondente à lista allow do projeto — ações futuras que casem saem auto_approved sem perguntar. Padrão já em deny continua bloqueado. A aprovação e o padrão são gravados na mesma transação. Clicar numa ação já APROVADA é sucesso idempotente (desfecho: ja_aprovada), gravando o padrão só se ele faltar.
| projectId required | string |
| sessionId required | string |
| actionId required | string |
{- "id": "01JC4Z8QK3M7YV2N5T9B0PXHRC",
- "projectId": "01JC4Z0000PROJETO0000000001",
- "sessionId": "01JC4Z8QK3M7YV2N5T9B0PXHRA",
- "seq": 7,
- "actionType": "terminal",
- "payload": {
- "command": "pnpm test"
}, - "status": "pending",
- "resolvedPolicy": "require_approval",
- "actor": {
- "kind": "agent",
- "id": "dev-api"
}, - "decidedBy": null,
- "decidedAt": null,
- "rejectionReason": null,
- "executionResult": null,
- "createdAt": "2026-07-27T14:33:10.900Z",
- "updatedAt": "2026-07-27T14:33:10.900Z",
- "desfecho": "aprovada",
- "padraoGravado": true
}O motivo, se dado, fica no event log junto com a decisão.
| projectId required | string |
| sessionId required | string |
| actionId required | string |
| reason | string Fica no event log junto com a recusa. Opcional, mas recomendado. |
{- "reason": "The command would delete the working directory."
}{- "id": "01JC4Z8QK3M7YV2N5T9B0PXHRC",
- "projectId": "01JC4Z0000PROJETO0000000001",
- "sessionId": "01JC4Z8QK3M7YV2N5T9B0PXHRA",
- "seq": 7,
- "actionType": "terminal",
- "payload": {
- "command": "pnpm test"
}, - "status": "pending",
- "resolvedPolicy": "require_approval",
- "actor": {
- "kind": "agent",
- "id": "dev-api"
}, - "decidedBy": null,
- "decidedAt": null,
- "rejectionReason": null,
- "executionResult": null,
- "createdAt": "2026-07-27T14:33:10.900Z",
- "updatedAt": "2026-07-27T14:33:10.900Z"
}O projeto nasce com as três áreas (RN-094), então a lista volta cheia mesmo antes de existir module_map. O que ativar a execução acrescenta são os MEMBROS da área de dev, um por módulo.
| projectId required | string |
[- {
- "id": "01JC4Z8QK3M7YV2N5T9B0PXHRA",
- "projectId": "01JC4Z8QK3M7YV2N5T9B0PXHRA",
- "key": "dev",
- "leadAgentId": "dev-lead",
- "maxParallel": 2,
- "budgetMicros": 20000000,
- "spentMicros": 4300000,
- "members": [
- "dev-api",
- "dev-web"
]
}
]Exige maintainer, pelo mesmo motivo do teto de paralelismo: é uma checagem INDEPENDENTE e aditiva ao lado dos orçamentos de projeto e de sessão (ADR 0110) — não é cascata, nem substitui nenhum dos dois. O limite entra em DÓLARES e é convertido para micro-USD no servidor, a mesma convenção do BudgetsController. null limpa o teto.
| projectId required | string |
| key required | string |
| limitUsd required | object or null >= 0 Teto em DÓLARES para a área — a mesma convenção dólar na entrada, micro-USD na saída dos orçamentos de projeto/sessão ( |
{- "limitUsd": 20
}{- "id": "01JC4Z8QK3M7YV2N5T9B0PXHRA",
- "projectId": "01JC4Z8QK3M7YV2N5T9B0PXHRA",
- "key": "dev",
- "leadAgentId": "dev-lead",
- "maxParallel": 2,
- "budgetMicros": 20000000,
- "spentMicros": 4300000,
- "members": [
- "dev-api",
- "dev-web"
]
}Exige maintainer, não developer, pelo mesmo motivo de ativar a execução: subir o teto é decidir quanto o produto pode gastar sem perguntar. Vale para os PRÓXIMOS pedidos — o que já aguarda a sua decisão continua aguardando.
| projectId required | string |
| key required | string |
| maxParallel required | number >= 1 Quantos agentes o lead da área pode ter na sessão SEM pedir a sua autorização. Mínimo 1 — zero não quer dizer "ilimitado", quer dizer configuração inválida, e tratá-lo como ilimitado transformaria um erro de digitação em gasto irrestrito. |
{- "maxParallel": 4
}{- "id": "01JC4Z8QK3M7YV2N5T9B0PXHRA",
- "projectId": "01JC4Z8QK3M7YV2N5T9B0PXHRA",
- "key": "dev",
- "leadAgentId": "dev-lead",
- "maxParallel": 2,
- "budgetMicros": 20000000,
- "spentMicros": 4300000,
- "members": [
- "dev-api",
- "dev-web"
]
}Exige maintainer, não developer, porque as ações de git dos dev agents herdam o papel de QUEM ATIVOU na avaliação de IAM — ativar como developer deixaria os agentes sem conseguir abrir PR. Cada módulo do module_map ganha um agente num worktree isolado.
| projectId required | string |
| taskBudgetMicros | number Orçamento por tarefa em micro-USD. Omitido usa o padrão do domínio (DEFAULT_TASK_BUDGET_MICROS). |
| maxGateCorrections | number Quantas idas e voltas dev↔gate antes de a tarefa virar |
| devAgentImpl | string Valores: "real" "noop"
|
| terminalAllowPatterns | Lista de strings Padrões liberados no |
| originSessionId | string Id da sessão de CHAT de onde veio o clique "ativar execução" — a sessão criativa/consultiva do Dev Lead/PO, nunca a de execução. Omitido (chamador legado, ex.: ativação pela Visão geral sem contexto de sessão) não fecha sessão nenhuma, como antes. Quando dado, ela é fechada no fim — mas só se não tiver handoff/ação/turno pendente (RN-135), e nunca se for a própria sessão de execução. |
{- "taskBudgetMicros": 500000,
- "maxGateCorrections": 3,
- "devAgentImpl": "real",
- "terminalAllowPatterns": [
- "Terminal(pnpm test:*)",
- "Terminal(pnpm lint)"
], - "originSessionId": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
}{- "sessionId": "01JC4Z8QK3M7YV2N5T9B0PXHRA",
- "modules": [
- "api",
- "web"
]
}A sessão active mais recente que já traz execution.activated — o MESMO critério que activate usa para decidir entre reativar ou criar uma (RN-139). null quando não há execução em andamento. Existe para a aba Executores parar de inferir esta sessão a partir da mais recente do projeto, que muda de sessão em sessão sem pista nenhuma na interface.
| projectId required | string |
{- "id": "01JC4Z8QK3M7YV2N5T9B0PXHRA",
- "projectId": "01JC4Z0000PROJETO0000000001",
- "createdBy": "01JC4Z0000USUARIO0000000001",
- "status": "active",
- "kind": "criativa",
- "name": "Cart checkout",
- "nextSeq": 42,
- "createdAt": "2026-07-27T14:03:22.187Z",
- "updatedAt": "2026-07-27T14:31:09.004Z",
- "closedAt": null,
- "terminationReason": null,
- "traceParent": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
}Zera o contador de tarefas bloqueadas consecutivas (RN-047) e devolve o agente a tentar reivindicar trabalho. É a única saída de idle_tripped — não há desbloqueio automático.
| projectId required | string |
| sessionId required | string |
| agentId required | string |
{- "ok": true
}Passa pelo TETO da área de dev (RN-083): abaixo dele, o agente sobe na hora, no próprio worktree; acima, a resposta é uma proposed_action do tipo parallelize aguardando a sua decisão, e NADA sobe até você decidir. O valor do teto é configurável por lead em Configurações.
| projectId required | string |
| sessionId required | string |
| module required | string Nome do módulo, como aparece no |
{- "module": "api"
}{- "estado": "aguardando_autorizacao",
- "actionId": "01JC4Z8QK3M7YV2N5T9B0PXHRA",
- "ativosNaSessao": 2,
- "maxParallel": 2,
- "motivo": "string"
}Zera o contador de correções do gate e devolve a tarefa ao agente. É a saída humana do teto de correções — não há desbloqueio automático.
| projectId required | string |
| sessionId required | string |
| taskId required | string |
{- "ok": true
}A mais recente primeiro, cada uma já trazendo o diff contra a anterior, calculado no servidor.
| projectId required | string |
| agent required | string Exemplo: dev-api |
[- {
- "id": "01JC4Z0000VERSAO00000000001",
- "version": 4,
- "content": "# dev-api\n\nAlways run the suite before opening the PR.",
- "createdBy": null,
- "sourceActionId": null,
- "sourceHypothesisId": "01JC4Z0000HIPOTESE000000001",
- "note": "Explicit definition-of-done criterion",
- "createdAt": "2026-07-26T14:00:00.000Z",
- "isCurrent": true,
- "diff": {
- "lines": [
- {
- "kind": "add",
- "content": "Always run the suite before opening the PR.",
- "lineNo": 42
}
], - "additions": 3,
- "deletions": 1
}
}
]O histórico é imutável: restaurar não apaga nada, CRIA uma versão nova com o conteúdo antigo. Exige maintainer porque muda o comportamento do agente dali em diante — mesmo calibre do patch.
| projectId required | string |
| agent required | string Exemplo: dev-api |
| version required | string Exemplo: 2 Versão a restaurar. |
{- "agent": "dev-api",
- "restoredFrom": 2,
- "toVersion": 5,
- "cacheInvalidated": true
}Diferente da rota de evento por sessão: aqui a sessão não é conhecida. É isto que faz o chip de evidência do perfil cair no evento certo, já que a janela da Anamnese atravessa várias sessões.
| projectId required | string |
| eventId required | string |
{- "id": "01JC4Z8QK3M7YV2N5T9B0PXHRB",
- "sessionId": "01JC4Z8QK3M7YV2N5T9B0PXHRA",
- "seq": 41,
- "type": "chat.message",
- "actor": {
- "kind": "agent",
- "id": "dev-api"
}, - "payload": {
- "role": "assistant",
- "content": "I'll open the epic first."
}, - "createdAt": "2026-07-27T14:30:58.512Z"
}Parte de quem TEM versão no projeto, não de uma lista estática — é isso que faz os dev agents por módulo (dev-api) aparecerem.
| projectId required | string |
[- {
- "agent": "dev-api",
- "versions": [
- {
- "id": "01JC4Z0000VERSAO00000000001",
- "version": 4,
- "content": "# dev-api\n\nAlways run the suite before opening the PR.",
- "createdBy": null,
- "sourceActionId": null,
- "sourceHypothesisId": "01JC4Z0000HIPOTESE000000001",
- "note": "Explicit definition-of-done criterion",
- "createdAt": "2026-07-26T14:00:00.000Z",
- "isCurrent": true,
- "diff": {
- "lines": [
- {
- "kind": "add",
- "content": "Always run the suite before opening the PR.",
- "lineNo": 42
}
], - "additions": 3,
- "deletions": 1
}
}
]
}
]O PRÓPRIO perfil do usuário por padrão; a visão agregada do time só para quem administra o projeto. Perfil de competência é dado SOBRE a pessoa, e o padrão menos surpreendente é ver o seu.
| projectId required | string |
[- {
- "id": "01JC4Z0000PERFIL00000000001",
- "projectId": "01JC4Z0000PROJETO0000000001",
- "userId": "01JC4Z0000USUARIO0000000002",
- "competency": "TypeScript",
- "level": "avancado",
- "rationale": "Fixed three generic typing errors without help.",
- "evidenceEventIds": [
- "01JC4Z8QK3M7YV2N5T9B0PXHRB"
], - "createdAt": "2026-07-26T12:00:00.000Z",
- "updatedAt": "2026-07-26T12:00:00.000Z",
- "userName": "Senior Dev",
- "userEmail": "dev@brabo.dev"
}
]Só o PRÓPRIO perfil de quem chama, nunca o de outra pessoa. O opt-out vem junto porque sem ele a próxima rodada re-derivaria tudo e a exclusão seria cosmética. É viewer de propósito: a perfilagem cobre todos os membros, então exigir developer deixaria um viewer perfilado sem conseguir apagar o próprio.
| projectId required | string |
{- "deleted": 2,
- "optedOut": true
}O mapa vigente é o de maior version (o histórico é imutável). As pendências são a validação cruzada história↔mapa: história sem módulo, ou apontando para módulo que não existe.
| projectId required | string |
{- "moduleMap": {
- "id": "01JC4Z0000MODULEMAP00000001",
- "projectId": "01JC4Z0000PROJETO0000000001",
- "sessionId": "01JC4Z8QK3M7YV2N5T9B0PXHRA",
- "modules": [
- {
- "name": "api",
- "stack": "NestJS + Drizzle",
- "responsibility": "Business rules and HTTP surface.",
- "dependsOn": [
- "db"
], - "resources": {
- "cpus": 2,
- "memoryMb": 4096,
- "pidsLimit": 512
}
}
], - "version": 3,
- "createdAt": "2026-07-24T16:00:00.000Z"
}, - "adrs": [
- {
- "actionId": "01JC4Z8QK3M7YV2N5T9B0PXHRC",
- "title": "0007 — Postgres queue instead of Redis",
- "status": "executed",
}
], - "pendencies": [
- {
- "storyId": "01JC4Z0000HISTORIA000000001",
- "title": "Add item to cart",
- "status": "ready",
- "reason": "missing_module",
- "missing": [
- "pagamentos"
]
}
], - "c4Diagram": {
- "status": "sem_diagrama",
- "diagrama": {
- "systemName": "Brabo",
- "systemDescription": "Agent-orchestrated engineering platform.",
- "actors": [
- {
- "name": "User",
- "type": "person",
- "description": "Whoever operates the product through the web."
}
], - "contextDiagram": "C4Context\n title Context Diagram -- Brabo\n ...",
- "containerDiagram": "C4Container\n title Container Diagram -- Brabo\n ..."
}, - "version": 0,
- "eventId": "01JC4Z0000EVENTO000000002",
- "createdAt": { }
}
}Aninhada e completa, numa chamada só. Histórias ARQUIVADAS (RN-727) não são listadas. O backlog é escrito por agentes, pelas rotas /internal/*.
| projectId required | string |
[- {
- "id": "01JC4Z0000EPICO000000000001",
- "projectId": "01JC4Z0000PROJETO0000000001",
- "sessionId": "01JC4Z8QK3M7YV2N5T9B0PXHRA",
- "title": "Shopping cart",
- "description": "Everything the buyer does before paying.",
- "createdAt": "2026-07-25T08:30:00.000Z",
- "updatedAt": "2026-07-25T08:30:00.000Z",
- "stories": [
- {
- "id": "01JC4Z0000HISTORIA000000001",
- "epicId": "01JC4Z0000EPICO000000000001",
- "projectId": "01JC4Z0000PROJETO0000000001",
- "sessionId": "01JC4Z8QK3M7YV2N5T9B0PXHRA",
- "title": "Add item to cart",
- "description": "As a buyer, I want to gather items before paying.",
- "rf": [
- "The cart accepts up to 50 items"
], - "rnf": [
- "The response stays under 200 ms at p95"
], - "businessRuleIds": [
- "RN-014"
], - "dod": [
- "Unit tests green"
], - "dor": [
- "Module defined"
], - "moduleIds": [
- "api"
], - "status": "ready",
- "proposedReady": true,
- "returnedReason": "DoD too generic — spell out the acceptance criteria.",
- "returnedAt": "2026-08-02T14:00:00.000Z",
- "archivedAt": null,
- "archivedReason": null,
- "createdAt": "2026-07-25T09:00:00.000Z",
- "updatedAt": "2026-07-25T09:40:00.000Z",
- "tasks": [
- {
- "id": "01JC4Z0000TAREFA00000000001",
- "storyId": "01JC4Z0000HISTORIA000000001",
- "title": "POST /cart/items endpoint",
- "description": "Accepts SKU and quantity; validates stock.",
- "status": "in_progress",
- "assignedTo": "dev-api",
- "blocked": false,
- "blockedReason": null,
- "blockedOrigin": null,
- "gateStatus": null,
- "gateCorrectionCount": 0,
- "module": "api",
- "createdAt": "2026-07-25T10:00:00.000Z",
- "updatedAt": "2026-07-25T12:30:00.000Z"
}
]
}
]
}
]Cruza as regras do projeto com os businessRuleIds das histórias. Regra sem história nenhuma é uma DESCOBERTA — pendência para o PO, não erro.
| projectId required | string |
{- "rules": [
- {
- "ruleId": "RN-014",
- "title": "Cart accepts at most 50 items",
- "coveredByStoryIds": [
- "01JC4Z0000HISTORIA000000001"
], - "covered": true
}
], - "uncoveredCount": 2
}Artefatos do InfraAgent (Dockerfile, compose, CI). Passam pelos MESMOS gates de QA e SecOps das PRs de dev, sem tarefa nem worktree por trás.
| projectId required | string |
[- {
- "id": "01JC4Z0000INFRAARTEFATO00001",
- "projectId": "01JC4Z0000PROJETO0000000001",
- "sessionId": "01JC4Z8QK3M7YV2N5T9B0PXHRA",
- "title": "api's production Dockerfile",
- "prActionId": "01JC4Z8QK3M7YV2N5T9B0PXHRD",
- "gateStatus": "awaiting_qa",
- "gateCorrectionCount": 0,
- "blocked": false,
- "blockedReason": null,
- "createdAt": "2026-07-26T09:00:00.000Z",
- "updatedAt": "2026-07-26T09:20:00.000Z"
}
]O status vem do event log: aberta é um artifact.open_question sem open_question.answered. A mais nova primeiro, teto de 50 com total.
| projectId required | string |
| status | string Padrão: "open" Valores: "open" "answered" "all" |
{- "itens": [
- {
- "questionId": "ffa0420e-f70b-4e5b-99a0-434bff860467",
- "eventId": "string",
- "sessionId": "string",
- "pergunta": "string",
- "sugestao": "string",
- "afeta": {
- "regras": [
- "string"
], - "historias": [
- "string"
], - "tarefas": [
- "string"
]
}, - "autor": "criativo",
- "criadaEm": "2019-08-24T14:15:22Z",
- "status": "open",
- "autorDaSessao": "string",
- "resposta": {
- "texto": "string",
- "aceitouSugestao": true,
- "decisionEventId": "string",
- "respondidaPor": "string",
- "respondidaEm": "2019-08-24T14:15:22Z"
}
}
], - "total": 0
}Só o autor da sessão onde a pergunta nasceu responde. Grava um decision_record (ator: o usuário, com questionId) e open_question.answered, e pede ao engine para retomar o gate de QA estacionado de cada tarefa afetada (RN-860), em melhor esforço.
| projectId required | string |
| questionId required | string |
| resposta | string Resposta em texto livre. Mande isto OU |
| aceitarSugestao | boolean Aceita a sugestão do agente como resposta. |
{- "resposta": "404 for a code that does not exist.",
- "aceitarSugestao": true
}{- "questionId": "ffa0420e-f70b-4e5b-99a0-434bff860467",
- "eventId": "string",
- "sessionId": "string",
- "pergunta": "string",
- "sugestao": "string",
- "afeta": {
- "regras": [
- "string"
], - "historias": [
- "string"
], - "tarefas": [
- "string"
]
}, - "autor": "criativo",
- "criadaEm": "2019-08-24T14:15:22Z",
- "status": "open",
- "autorDaSessao": "string",
- "resposta": {
- "texto": "string",
- "aceitouSugestao": true,
- "decisionEventId": "string",
- "respondidaPor": "string",
- "respondidaEm": "2019-08-24T14:15:22Z"
}
}RN-852 (ADR 0213): o artifact.run_instructions vigente, emitido quando um open_infra_pr com run executa. DECLARADO pelo agente de Infra, nunca verificado na máquina de quem lê. null quando não há.
| projectId required | string |
{- "command": "cp .env.example .env && docker compose up --build",
- "envExample": true,
- "app": {
- "command": "npm start",
- "port": 3000
}, - "qa": { },
- "version": 1,
- "eventId": "01JC4Z0000EVENTORUNINSTRUC01",
- "declaredAt": "2026-10-11T12:00:00.000Z"
}A história sai do backlog, da cobertura, da fila de promoção, do plano do Dev Lead e do claim — e as tarefas dela também. Nada é apagado: a linha fica com archivedAt, e backlog.story_archived registra quem, por quê e quais tarefas. Só história draft sem tarefa em execução.
| projectId required | string |
| storyId required | string |
| reason | string Opcional; gravado em |
{- "reason": "Duplicate of \"Register a user\"."
}{- "id": "01JC4Z0000HISTORIA000000001",
- "epicId": "01JC4Z0000EPICO000000000001",
- "projectId": "01JC4Z0000PROJETO0000000001",
- "sessionId": "01JC4Z8QK3M7YV2N5T9B0PXHRA",
- "title": "Add item to cart",
- "description": "As a buyer, I want to gather items before paying.",
- "rf": [
- "The cart accepts up to 50 items"
], - "rnf": [
- "The response stays under 200 ms at p95"
], - "businessRuleIds": [
- "RN-014"
], - "dod": [
- "Unit tests green"
], - "dor": [
- "Module defined"
], - "moduleIds": [
- "api"
], - "status": "ready",
- "proposedReady": true,
- "returnedReason": "DoD too generic — spell out the acceptance criteria.",
- "returnedAt": "2026-08-02T14:00:00.000Z",
- "archivedAt": null,
- "archivedReason": null,
- "createdAt": "2026-07-25T09:00:00.000Z",
- "updatedAt": "2026-07-25T09:40:00.000Z"
}A história sai da fila de proposta e o motivo vira uma mensagem fixada na sessão do PO — o mesmo padrão de devolver um gate ao dev. Se o PO daquela sessão não está mais de pé, a devolução é gravada mesmo assim: é decisão do usuário e não depende de um agente ouvindo.
| projectId required | string |
| storyId required | string |
| reason required | string O motivo é OBRIGATÓRIO: vira uma mensagem fixada na sessão do PO, e uma devolução sem motivo devolve trabalho sem informação. |
{- "reason": "The acceptance criteria don't cover the payment refusal case."
}{- "ok": true
}Só história draft sem tarefa em execução (in_progress/in_review). Grava backlog.story_updated com o título anterior e o novo. Mesmo papel das outras escritas do backlog (promover/devolver).
| projectId required | string |
| storyId required | string |
| title required | string Aparado; de 1 a 200 caracteres. |
{- "title": "Register a user"
}{- "id": "01JC4Z0000HISTORIA000000001",
- "epicId": "01JC4Z0000EPICO000000000001",
- "projectId": "01JC4Z0000PROJETO0000000001",
- "sessionId": "01JC4Z8QK3M7YV2N5T9B0PXHRA",
- "title": "Add item to cart",
- "description": "As a buyer, I want to gather items before paying.",
- "rf": [
- "The cart accepts up to 50 items"
], - "rnf": [
- "The response stays under 200 ms at p95"
], - "businessRuleIds": [
- "RN-014"
], - "dod": [
- "Unit tests green"
], - "dor": [
- "Module defined"
], - "moduleIds": [
- "api"
], - "status": "ready",
- "proposedReady": true,
- "returnedReason": "DoD too generic — spell out the acceptance criteria.",
- "returnedAt": "2026-08-02T14:00:00.000Z",
- "archivedAt": null,
- "archivedReason": null,
- "createdAt": "2026-07-25T09:00:00.000Z",
- "updatedAt": "2026-07-25T09:40:00.000Z"
}O passo humano que o modo manual (padrão desde a Fase 12c) devolve ao usuário: até a promoção, NENHUMA tarefa da história é pegável por dev agent algum. Promover libera o lote de tarefas de uma vez e acorda os agentes ociosos do módulo (Fase 12b). O lote NÃO é tudo ou nada — o que não passou volta como failed com o motivo, e o que passou é promovido.
| projectId required | string |
| storyIds required | Lista de strings Ids das histórias a promover para |
{- "storyIds": [
- "01JC2XK9Q7",
- "01JC2XKB4M"
]
}{- "promoted": [
- "string"
], - "failed": [
- {
- "storyId": "01JC2XK9Q7",
- "reason": "Story is not ready: missing dod, dor"
}
]
}Cada hipótese cita os eventos que a sustentam (evidenceEventIds) — é isso que a torna auditável em vez de aceita por fé.
| projectId required | string |
[- {
- "id": "01JC4Z0000HIPOTESE000000001",
- "projectId": "01JC4Z0000PROJETO0000000001",
- "sessionId": "01JC4Z8QK3M7YV2N5T9B0PXHRA",
- "analysisId": "01JC4Z0000ANALISE0000000001",
- "agenteAlvo": "dev-api",
- "observacao": "The agent reopened the same task three times.",
- "hipotese": "The instructions don't say when to consider the task done.",
- "sugestao": "Add an explicit definition-of-done criterion to the instruction.",
- "confiancaPercent": 72,
- "evidenceEventIds": [
- "01JC4Z8QK3M7YV2N5T9B0PXHRB"
], - "terminationAnalysis": {
- "causa": "heartbeat_timeout",
- "estadoDaSessao": "closed_abnormally",
- "analise": "The agent stopped responding after a 90s tool call."
}, - "status": "proposed",
- "decidedBy": null,
- "decidedAt": null,
- "createdAt": "2026-07-26T18:00:00.000Z",
- "updatedAt": "2026-07-26T18:00:00.000Z"
}
]Move proposed → accepted. NÃO é proposed_action: o Psicólogo é somente leitura e nada aqui tem efeito externo.
| projectId required | string |
| hypothesisId required | string |
{- "id": "01JC4Z0000HIPOTESE000000001",
- "projectId": "01JC4Z0000PROJETO0000000001",
- "sessionId": "01JC4Z8QK3M7YV2N5T9B0PXHRA",
- "analysisId": "01JC4Z0000ANALISE0000000001",
- "agenteAlvo": "dev-api",
- "observacao": "The agent reopened the same task three times.",
- "hipotese": "The instructions don't say when to consider the task done.",
- "sugestao": "Add an explicit definition-of-done criterion to the instruction.",
- "confiancaPercent": 72,
- "evidenceEventIds": [
- "01JC4Z8QK3M7YV2N5T9B0PXHRB"
], - "terminationAnalysis": {
- "causa": "heartbeat_timeout",
- "estadoDaSessao": "closed_abnormally",
- "analise": "The agent stopped responding after a 90s tool call."
}, - "status": "proposed",
- "decidedBy": null,
- "decidedAt": null,
- "createdAt": "2026-07-26T18:00:00.000Z",
- "updatedAt": "2026-07-26T18:00:00.000Z"
}Move proposed → dismissed. Mesmo raciocínio do aceite.
| projectId required | string |
| hypothesisId required | string |
{- "id": "01JC4Z0000HIPOTESE000000001",
- "projectId": "01JC4Z0000PROJETO0000000001",
- "sessionId": "01JC4Z8QK3M7YV2N5T9B0PXHRA",
- "analysisId": "01JC4Z0000ANALISE0000000001",
- "agenteAlvo": "dev-api",
- "observacao": "The agent reopened the same task three times.",
- "hipotese": "The instructions don't say when to consider the task done.",
- "sugestao": "Add an explicit definition-of-done criterion to the instruction.",
- "confiancaPercent": 72,
- "evidenceEventIds": [
- "01JC4Z8QK3M7YV2N5T9B0PXHRB"
], - "terminationAnalysis": {
- "causa": "heartbeat_timeout",
- "estadoDaSessao": "closed_abnormally",
- "analise": "The agent stopped responding after a 90s tool call."
}, - "status": "proposed",
- "decidedBy": null,
- "decidedAt": null,
- "createdAt": "2026-07-26T18:00:00.000Z",
- "updatedAt": "2026-07-26T18:00:00.000Z"
}costMicros é somado do token_usage da rodada. É isso que torna VISÍVEL a diferença de custo entre triagem leve e pesada, em vez de só afirmada — as duas faixas usam modelos de fato diferentes.
| projectId required | string |
[- {
- "id": "01JC4Z0000ANALISE0000000001",
- "projectId": "01JC4Z0000PROJETO0000000001",
- "sessionId": "01JC4Z8QK3M7YV2N5T9B0PXHRA",
- "tier": "leve",
- "triggeredBy": "auto",
- "supersedes": null,
- "superseded": false,
- "supersededAt": null,
- "eventCountAtAnalysis": 128,
- "createdAt": "2026-07-26T18:00:00.000Z",
- "costMicros": 4200,
- "hypothesisCount": 3
}
]Somente leitura — não tem efeito colateral. enabled: false quer dizer que a pausa é decisão de PRODUTO (não bug): análises e hipóteses existentes não são tocadas por ela.
{- "enabled": false
}Exige maintainer, não developer, porque roda o ToolLoop de novo e GASTA orçamento real — ao contrário de aceitar ou descartar, que só mexem em estado local. A análise anterior é marcada como superada.
| projectId required | string |
| sessionId required | string |
{- "ok": true
}Contagem real (nunca estimada) de arquivos .md sob docs/docs/adr no repositório do projeto contra quantos têm chunk, e das sessões do projeto contra quantas têm chunk. Não inclui "há N minutos" para esses três escopos — não há coluna de carimbo de indexação por escopo, e um número chutado mentiria (mesma régua do ADR 0042 para nota de modelo). local (RN-455) é a única exceção: lastAttachedAt é um MAX(created_at) real sobre aquele escopo, porque upload pelo navegador não tem estado de repositório contra o qual comparar.
| projectId required | string |
{- "docs": {
- "filesInRepo": 0,
- "filesIndexed": 0,
- "truncated": true
}, - "adr": {
- "filesInRepo": 0,
- "filesIndexed": 0,
- "truncated": true
}, - "session": {
- "sessionsInProject": 0,
- "sessionsIndexed": 0
}, - "local": {
- "filesIndexed": 0,
- "folderName": "string",
- "lastAttachedAt": "string"
}, - "chunksTotal": 0,
- "chunksWithoutVector": 0
}O único sinal de VERDADE que a medição do RAG tem: latência e taxa de degradação dizem se a busca RODOU, nunca se ACERTOU. Mesmo papel de search — quem pode ler o resultado é quem pode julgá-lo. Votar de novo no mesmo chunk da mesma busca sobrescreve o próprio voto em vez de somar um segundo (único por ator), para a métrica medir acerto e não entusiasmo.
| projectId required | string |
| searchId required | string <uuid> O |
| chunkId required | string <uuid> O chunk sendo julgado. Tem de ser um dos acertos que AQUELA busca devolveu, senão 400. |
| verdict required | string Valores: "util" "irrelevante" Dois valores, não uma escala de 1 a 5: uma escala mais fina convida diferenças de régua entre votantes que nenhuma agregação recupera depois. |
{- "searchId": "9b1a67f9-9477-48e5-8a6c-11b70245e1d9",
- "chunkId": "90e1f3df-a8a1-438e-8dd5-10e45398d051",
- "verdict": "util"
}{- "searchId": "9b1a67f9-9477-48e5-8a6c-11b70245e1d9",
- "chunkId": "90e1f3df-a8a1-438e-8dd5-10e45398d051",
- "verdict": "util",
- "rank": 0
}Conteúdo de texto já lido pelo navegador (<input webkitdirectory>), nunca um caminho do host — essa distinção é todo o ponto do ADR 0113. Reconstrução completa e idempotente do escopo local: toda chamada o apaga e recria a partir dos arquivos enviados DESTA vez, então reenviar é o mecanismo de ressincronização. Recusa (400) acima dos tetos de quantidade de arquivos/bytes totais em vez de cortar em silêncio — é um gesto único do usuário com um seletor de pasta à frente, não uma varredura em segundo plano. Arquivo individual grande demais ou de extensão não reconhecida é só pulado, nunca reprova o lote.
| projectId required | string |
| folderName required | string O nome da pasta como escolhido no navegador — mostrado no painel de cobertura. |
required | Lista de objects (LocalFolderFileDto) Até 500 arquivos. O teto exato de bytes somados (RAG_LOCAL_TOTAL_BYTES_LIMIT) e o allowlist de extensões por arquivo são aplicados pelo caso de uso, não por este DTO — um array de strings individualmente válidas ainda pode somar um payload grande demais. |
{- "folderName": "brabo",
- "files": [
- {
- "path": "src/domain/rag/chunking.ts",
- "content": "string"
}
]
}{- "folderName": "string",
- "filesIndexed": 0,
- "filesSkipped": 0,
- "chunksCreated": 0,
- "embedding": {
- "available": true,
- "embedded": 0,
- "skipped": 0,
- "reason": "string"
}
}"Reindexar agora" do painel de RAG do Chat. Apaga e recria os chunks dos três escopos honestos (RN-219) a partir do estado CORRENTE — não há watcher automático em push/fechamento de sessão (decisão registrada nos ADRs 0079/0080). Pode demorar em projetos com muitas sessões: roda uma passada de indexação por sessão, cada uma possivelmente chamando o provider de embedding em lote.
| projectId required | string |
{- "docs": {
- "filesScanned": 0,
- "docsChunks": 0,
- "adrChunks": 0,
- "truncated": true,
- "embedding": {
- "available": true,
- "embedded": 0,
- "skipped": 0,
- "reason": "string"
}
}, - "sessions": {
- "total": 0,
- "indexed": 0,
- "chunksCreated": 0
}, - "embeddingAvailable": true,
- "embeddingReason": "string"
}Combina similaridade de cosseno (pgvector) com ts_rank léxico (tsvector), cada um de uma consulta independente, mesclados por soma ponderada e cortados pelo limiar (ADR 0080). vectorAvailable: false avisa quando o provider de embedding não respondeu e a busca rodou só com o sinal léxico — nunca finge ter rodado o híbrido completo.
| projectId required | string |
| query required | string Entre 2 e 500 caracteres — mesma régua da busca da aba Código. |
| scopes | Lista de strings Itens Valores: "docs" "adr" "session" "local" Quando ausente, busca nos quatro escopos (RN-219/454). |
| limit | number De 1 a 10 — quando ausente, usa o teto. |
{- "query": "how does the PR gate work",
- "scopes": [
- "docs"
], - "limit": 10
}{- "query": "string",
- "searchId": "9b1a67f9-9477-48e5-8a6c-11b70245e1d9",
- "hits": [
- {
- "chunkId": "string",
- "scope": "docs",
- "content": "string",
- "score": 0,
- "vectorScore": 0,
- "lexicalScore": 0,
- "origin": { }
}
], - "vectorAvailable": true,
- "vectorUnavailableReason": "string"
}Cada item traz technical: true só para a sessão aberta pelo provisionamento do repositório (ligada por repo_bootstraps).
| projectId required | string |
[- {
- "id": "01JC4Z8QK3M7YV2N5T9B0PXHRA",
- "projectId": "01JC4Z0000PROJETO0000000001",
- "createdBy": "01JC4Z0000USUARIO0000000001",
- "status": "active",
- "kind": "criativa",
- "name": "Cart checkout",
- "nextSeq": 42,
- "createdAt": "2026-07-27T14:03:22.187Z",
- "updatedAt": "2026-07-27T14:31:09.004Z",
- "closedAt": null,
- "terminationReason": null,
- "traceParent": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01",
- "technical": false
}
]A sessão nasce em created e é o contêiner de tudo o que os agentes fazem: o event log, as ações propostas e o orçamento de tokens ficam pendurados nela. kind é OBRIGATÓRIO e fica gravado: consultiva só conversa, criativa produz e é a única que aceita execution.activated.
| projectId required | string |
| kind required | string Valores: "consultiva" "criativa" A intenção da sessão. |
| name | string <= 80 characters Nome amigável, opcional. Não substitui a hashtag do id — as telas compõem os dois. Em branco conta como ausente. |
{- "kind": "criativa",
- "name": "Cart checkout"
}{- "id": "01JC4Z8QK3M7YV2N5T9B0PXHRA",
- "projectId": "01JC4Z0000PROJETO0000000001",
- "createdBy": "01JC4Z0000USUARIO0000000001",
- "status": "active",
- "kind": "criativa",
- "name": "Cart checkout",
- "nextSeq": 42,
- "createdAt": "2026-07-27T14:03:22.187Z",
- "updatedAt": "2026-07-27T14:31:09.004Z",
- "closedAt": null,
- "terminationReason": null,
- "traceParent": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
}| projectId required | string |
| sessionId required | string |
{- "id": "01JC4Z8QK3M7YV2N5T9B0PXHRA",
- "projectId": "01JC4Z0000PROJETO0000000001",
- "createdBy": "01JC4Z0000USUARIO0000000001",
- "status": "active",
- "kind": "criativa",
- "name": "Cart checkout",
- "nextSeq": 42,
- "createdAt": "2026-07-27T14:03:22.187Z",
- "updatedAt": "2026-07-27T14:31:09.004Z",
- "closedAt": null,
- "terminationReason": null,
- "traceParent": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
}O nome é rótulo de navegação e NÃO substitui a hashtag do id — as telas compõem os dois, e sem nome degradam para só a hashtag. null ou texto em branco limpa o nome. Não há como mudar o kind: é a intenção de criação, e mudá-lo depois o transformaria em estado.
| projectId required | string |
| sessionId required | string |
| name required | object or null <= 80 characters O nome novo. |
{- "name": "Cart checkout"
}{- "id": "01JC4Z8QK3M7YV2N5T9B0PXHRA",
- "projectId": "01JC4Z0000PROJETO0000000001",
- "createdBy": "01JC4Z0000USUARIO0000000001",
- "status": "active",
- "kind": "criativa",
- "name": "Cart checkout",
- "nextSeq": 42,
- "createdAt": "2026-07-27T14:03:22.187Z",
- "updatedAt": "2026-07-27T14:31:09.004Z",
- "closedAt": null,
- "terminationReason": null,
- "traceParent": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
}Human gesture, with no cap: the automatic resume happens once at boot (ADR 0226) and this is the button for the rest. The reply is the ACCEPT (the turn goes up in the engine); progress arrives on the session channel. Requires developer.
| projectId required | string |
| sessionId required | string |
| agent required | string |
{- "ok": true
}Regras de negócio, registros de decisão e artefatos por agente contados sobre o log INTEIRO da sessão (a tela lê uma cauda de 200 eventos), mais os arquivos tocados por git_commit/git_push nesta sessão e na sessão de execução que o plano aprovado do Dev Lead dela ativou (RN-828). Desde a RN-844 devolve também os ITENS de regras e decisões (os 50 mais recentes de cada; os totais ficam em regras/decisoes) e os arquivos das PRs abertas nessas sessões, lidos do mesmo diff que a aba de PRs lê, para no máximo 20 PRs por chamada; prsNaoLidas diz quantas ficaram de fora ou falharam. Só leitura; nenhuma tabela nova.
| projectId required | string |
| sessionId required | string |
{- "sessoesLidas": [
- "string"
], - "regras": 11,
- "decisoes": 6,
- "itensDeRegras": [
- {
- "eventId": "evt_01JC4Z",
- "seq": 42,
- "actorId": "po",
- "titulo": "Login exige e-mail",
- "descricao": "string",
- "origens": 2
}
], - "itensDeDecisoes": [
- {
- "eventId": "evt_01JC4Z",
- "seq": 43,
- "actorId": "criativo",
- "escolha": "SQLite em arquivo",
- "contexto": "string"
}
], - "artefatosPorAgente": [
- {
- "actorId": "po",
- "quantos": 7
}
], - "arquivosTocados": {
- "itens": [
- {
- "actionId": "01JC4Z0000ACAO00000000000001",
- "path": "src/app.ts",
- "additions": 12,
- "deletions": 3
}
], - "total": 140,
- "prsNaoLidas": 0
}
}O log é IMUTÁVEL e ordenado por seq. A paginação normal é incremental: passe o nextCursor da resposta anterior como afterSeq. latest=true faz o contrário — busca a CAUDA e ignora afterSeq, para uma tela que abre no fim em vez de reconstruir a sessão inteira desde o começo (ADR 0021).
| projectId required | string |
| sessionId required | string |
| afterSeq | string Exemplo: afterSeq=40 Devolve os eventos com |
| limit | string Exemplo: limit=50 |
| latest | string Exemplo: latest=true Busca a cauda do log; ignora |
| actionId | string Exemplo: actionId=01JC4Z0000ACAO00000000000001 Só os eventos cujo |
{- "items": [
- {
- "id": "01JC4Z8QK3M7YV2N5T9B0PXHRB",
- "sessionId": "01JC4Z8QK3M7YV2N5T9B0PXHRA",
- "seq": 41,
- "type": "chat.message",
- "actor": {
- "kind": "agent",
- "id": "dev-api"
}, - "payload": {
- "role": "assistant",
- "content": "I'll open the epic first."
}, - "createdAt": "2026-07-27T14:30:58.512Z"
}
], - "nextCursor": 41
}Append-only: o seq é atribuído pelo servidor e nada no log é atualizado ou removido depois. Não há endpoint de edição, por desenho.
| projectId required | string |
| sessionId required | string |
| type required | string Tipo do evento. O inventário completo está em |
required | object (ActorDto) |
required | object Forma livre, específica de cada |
{- "type": "chat.message",
- "actor": {
- "kind": "agent",
- "id": "dev-api"
}, - "payload": {
- "role": "user",
- "content": "I want a new checkout."
}
}{- "id": "01JC4Z8QK3M7YV2N5T9B0PXHRB",
- "sessionId": "01JC4Z8QK3M7YV2N5T9B0PXHRA",
- "seq": 41,
- "type": "chat.message",
- "actor": {
- "kind": "agent",
- "id": "dev-api"
}, - "payload": {
- "role": "assistant",
- "content": "I'll open the epic first."
}, - "createdAt": "2026-07-27T14:30:58.512Z"
}Existe para que a evidência das hipóteses do Psicólogo alcance o evento citado não importa em que página ele caiu nem os filtros do feed.
| projectId required | string |
| sessionId required | string |
| eventId required | string |
{- "id": "01JC4Z8QK3M7YV2N5T9B0PXHRB",
- "sessionId": "01JC4Z8QK3M7YV2N5T9B0PXHRA",
- "seq": 41,
- "type": "chat.message",
- "actor": {
- "kind": "agent",
- "id": "dev-api"
}, - "payload": {
- "role": "assistant",
- "content": "I'll open the epic first."
}, - "createdAt": "2026-07-27T14:30:58.512Z"
}Só closed/closed_abnormally reabrem, e só para active — closing nunca volta (ADR 0183). A sessão mantém o event log, os artefatos, as perguntas respondidas e os handoffs; o kind não é tocado. Um evento NOVO session.reopened registra o closedAt e o terminationReason anteriores, que a linha limpa. Sessão que traz execution.activated é recusada (sessao_com_execucao): abra uma sessão nova e ative a execução nela. Sem prazo. Exige developer (ADR 0184).
| projectId required | string |
| sessionId required | string |
{- "id": "01JC4Z8QK3M7YV2N5T9B0PXHRA",
- "projectId": "01JC4Z0000PROJETO0000000001",
- "createdBy": "01JC4Z0000USUARIO0000000001",
- "status": "active",
- "kind": "criativa",
- "name": "Cart checkout",
- "nextSeq": 42,
- "createdAt": "2026-07-27T14:03:22.187Z",
- "updatedAt": "2026-07-27T14:31:09.004Z",
- "closedAt": null,
- "terminationReason": null,
- "traceParent": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
}Resolvido para QUEM CHAMA, nunca para a sessão: sobreposição da sessão > escolha da Conta > detectado-e-confirmado > idioma da interface (RN-618). Devolve o vencedor, a origem dele e cada elo da cadeia — e, quando as suas mensagens recentes apontam para outro idioma e a tela deveria perguntar sobre isso, detectionQuestion (RN-624).
| projectId required | string |
| sessionId required | string |
{- "language": "pt-BR",
- "origin": "interface",
- "sessionOverride": null,
- "account": "automatico",
- "detected": null,
- "interfaceLocale": "pt-BR",
- "detectionQuestion": "es"
}Só para quem chama, só nesta sessão (RN-618) — os outros participantes e a sua escolha na Conta não são tocados. null volta a herdar da Conta.
| projectId required | string |
| sessionId required | string |
| language required | string or null <= 35 characters O idioma em que os agentes respondem a VOCÊ, só NESTA sessão (RN-618) — os outros participantes nunca são afetados. Qualquer código BCP-47 que o servidor reconhece, gravado na forma canônica. Mande |
{- "language": "en"
}{- "language": "pt-BR",
- "origin": "interface",
- "sessionOverride": null,
- "account": "automatico",
- "detected": null,
- "interfaceLocale": "pt-BR",
- "detectionQuestion": "es"
}O ticket autentica o socket Phoenix session:<id> da sessão, no connect/3 do engine — NÃO é o JWT reaproveitado. TTL de 30s e uso único: toda reconexão (inclusive as automáticas) pede um ticket novo. scope: "heartbeat" exige o papel viewer; scope: "terminal" exige developer, o mesmo papel mínimo das ações de terminal.
| projectId required | string |
| sessionId required | string |
| scope required | string Valores: "heartbeat" "terminal"
|
{- "scope": "heartbeat"
}{- "ticket": "string",
- "expiresAt": "2026-08-09T12:00:30.000Z"
}A transição é validada pela máquina de estados do domínio, não pelo cliente: created → active → closing → closed | closed_abnormally. Salto inválido é 409.
| projectId required | string |
| sessionId required | string |
| status required | string Valores: "created" "active" "closing" "closed" "closed_abnormally" Estado alvo. A máquina de estados recusa salto inválido com 409 — de |
{- "status": "closing"
}{- "id": "01JC4Z8QK3M7YV2N5T9B0PXHRA",
- "projectId": "01JC4Z0000PROJETO0000000001",
- "createdBy": "01JC4Z0000USUARIO0000000001",
- "status": "active",
- "kind": "criativa",
- "name": "Cart checkout",
- "nextSeq": 42,
- "createdAt": "2026-07-27T14:03:22.187Z",
- "updatedAt": "2026-07-27T14:31:09.004Z",
- "closedAt": null,
- "terminationReason": null,
- "traceParent": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
}O botão "Parar" do composer (RN-122): mata a chamada de LLM em voo no engine, cortando a conexão no meio do stream — economiza tokens de verdade, não só para de renderizar no cliente. Idempotente: sem turno em andamento, é aceito sem efeito.
| projectId required | string |
| sessionId required | string |
| agent required | string Exemplo: po Slug do agente ativo. |
{- "ok": true
}A resposta é só a confirmação, e volta no ACEITE — antes de o turno do agente terminar (ADR 0163). O que o agente responde chega pelo event log e pelo canal da sessão — não por esta chamada. Se o agente está no meio de um turno, a mensagem NÃO é recusada: entra na fila do agente (entrega: "enfileirada") e é lida, com as outras enfileiradas, num turno só quando o atual terminar; pode ser cancelada enquanto espera (RN-673, ADR 0191).
| projectId required | string |
| sessionId required | string |
| agent required | string Exemplo: po Slug do agente conversacional que lê a mensagem: criativo, po, arquiteto, dev-lead, ux-designer ou staff. Qualquer outro slug — infra inclusive — é recusado com 422; nunca é entregue a um agente padrão (RN-584). |
| text required | string [ 1 .. 10000 ] characters Mensagem do usuário ao agente ativo da sessão. |
{- "text": "I want to allow discount coupons at checkout."
}{- "ok": true,
- "mensagemId": "01JC4Z0000EVENTO000000000001",
- "entrega": "enfileirada",
- "posicao": 2
}Mensagem enviada com o agente no meio de um turno espera numa fila e é lida quando o turno termina (RN-673). Enquanto espera, quem a ENVIOU pode cancelá-la: o engine a tira da fila e grava chat.message_cancelled; ela nunca é mostrada ao modelo. Mensagem de outra pessoa é 403.
| projectId required | string |
| sessionId required | string |
| agent required | string Exemplo: po Slug do agente em cuja fila está a mensagem. |
| messageId required | string Exemplo: 01JC4Z0000EVENTO000000000001 Id do evento |
{- "ok": true
}Só o Criativo pode ser iniciado por comando direto. Todo outro agente exige um handoff ACEITO apontando para ele — a regra mora no domínio, não aqui, e tentar contorná-la devolve 409.
| projectId required | string |
| sessionId required | string |
| agent required | string Exemplo: criativo Slug do agente. |
{- "agent": "po",
- "status": "active"
}Grava chat.structured_question_answered e reenvia as respostas ao agente como mensagem normal. Um conjunto de perguntas só pode ser respondido uma vez. Volta no ACEITE, antes de o turno do agente terminar (ADR 0163): o turno segue rodando no engine e a narração, o fim e as falhas dele chegam pelo canal da sessão e pelo event log (agent.status, agent.response, agent.error) — nunca por esta resposta.
| projectId required | string |
| sessionId required | string |
| agent required | string Exemplo: criativo Slug do agente que fez as perguntas (e vai ler a resposta). |
| questionSetId required | string Exemplo: 01JC4Z0000EVENTO000000000001 Id do evento |
required | object Resposta indexada pelo | ||
| |||
{- "answers": {
- "name": "Easy Checkout",
- "users": "Small business owners",
- "platform": "Web"
}
}{- "ok": true
}Endpoint dedicado em vez de reusar readiness, que é do Criativo: são dois marcos diferentes da sessão, e confundi-los tornaria o event log ambíguo. Volta no ACEITE, antes de o turno do agente terminar (ADR 0163): o turno segue rodando no engine e a narração, o fim e as falhas dele chegam pelo canal da sessão e pelo event log (agent.status, agent.response, agent.error) — nunca por esta resposta. O handoff ao Dev Lead continua vindo DEPOIS do de Infra: o engine o segura até o turno de fechamento terminar. Idempotente (ADR 0182, RN-635): alvo que já tem oferta pendente ou está ativo no projeto não é disparado de novo, e com os dois assim nada é gravado.
| projectId required | string |
| sessionId required | string |
{- "ok": true,
- "desfecho": "confirmado",
- "jaAtendidos": [
- {
- "toAgent": "infra",
- "motivo": "oferta_pendente"
}
]
}Grava necessity.validated. Exige que o Criativo já tenha consolidado um product_brief nesta sessão (RN-406) — sem ele, é recusado: não há o que validar. Desde o ADR 0185 (RN-657) o clique "Estou pronto — a necessidade está validada" (POST .../readiness) grava ele mesmo este evento, e a web não chama mais esta rota; ela fica para sessões cujo clique de prontidão é anterior a esse ADR.
| projectId required | string |
| sessionId required | string |
{- "ok": true
}| projectId required | string |
| sessionId required | string |
[- {
- "id": "01JC4Z0000HANDOFF00000000001",
- "sessionId": "01JC4Z8QK3M7YV2N5T9B0PXHRA",
- "projectId": "01JC4Z0000PROJETO0000000001",
- "fromAgent": "criativo",
- "toAgent": "po",
- "artifactId": "01JC4Z0000ARTEFATO000000001",
- "status": "offered",
- "createdAt": "2026-07-24T10:00:00.000Z",
- "updatedAt": "2026-07-24T10:05:00.000Z"
}
]Nasce offered, exatamente como o offer_handoff de um agente — a única diferença é quem decidiu. toAgent tem de estar no catálogo endereçável (lead de área ou agente sem área); subagente ou slug desconhecido é recusado com 400. No máximo uma oferta pendente por (projeto, alvo): ver desfecho (ADR 0182, RN-635).
| projectId required | string |
| sessionId required | string |
| toAgent required | string Slug do agente alvo. Tem de ser um lead ou um agente sem área — nunca um subagente. |
{- "toAgent": "staff"
}{- "id": "01JC4Z0000HANDOFF00000000001",
- "sessionId": "01JC4Z8QK3M7YV2N5T9B0PXHRA",
- "projectId": "01JC4Z0000PROJETO0000000001",
- "fromAgent": "criativo",
- "toAgent": "po",
- "artifactId": "01JC4Z0000ARTEFATO000000001",
- "status": "offered",
- "createdAt": "2026-07-24T10:00:00.000Z",
- "updatedAt": "2026-07-24T10:05:00.000Z",
- "desfecho": "criado"
}Uma coisa só: transicionar para accepted E iniciar o agente de destino. É o caminho normal de ativação de todo agente que não é o Criativo.
| projectId required | string |
| sessionId required | string |
| handoffId required | string |
{- "id": "01JC4Z0000HANDOFF00000000001",
- "sessionId": "01JC4Z8QK3M7YV2N5T9B0PXHRA",
- "projectId": "01JC4Z0000PROJETO0000000001",
- "fromAgent": "criativo",
- "toAgent": "po",
- "artifactId": "01JC4Z0000ARTEFATO000000001",
- "status": "offered",
- "createdAt": "2026-07-24T10:00:00.000Z",
- "updatedAt": "2026-07-24T10:05:00.000Z"
}É o botão que dispara o product_brief e o handoff ao PO. Grava readiness.confirmed no event log. Volta no ACEITE, antes de o turno do agente terminar (ADR 0163): o turno segue rodando no engine e a narração, o fim e as falhas dele chegam pelo canal da sessão e pelo event log (agent.status, agent.response, agent.error) — nunca por esta resposta.
| projectId required | string |
| sessionId required | string |
{- "ok": true
}Authenticated by a MACHINE device key (never a session JWT, never a project-bound credential). Same envelope encryption as POST /users/me/credentials. 409 credencial_existente when the user already has this provider and substituir is not true. The key is never logged nor returned.
| provider required | string Valores: "anthropic" "openai" "openrouter" "nvidia-nim" "together" "deepinfra" "bitdeer" "vultr" "9router" "opencode-zen" |
| apiKey required | string <= 512 characters The API key in plain text. Encrypted before touching the database; NEVER logged and NEVER returned. |
| origem required | string <= 200 characters Tool and path the key was read from. Audit log only; never a value. |
| substituir | boolean Must be |
{- "provider": "anthropic",
- "apiKey": "string",
- "origem": "codex: ~/.codex/auth.json",
- "substituir": true
}{- "provider": "anthropic",
- "substituida": true
}Nenhum papel exigido: credencial é sobre quem chamou, não sobre um workspace ou projeto. Cobre chaves de LLM e tokens de git na mesma listagem.
[- {
- "id": "01JC4Z0000CREDENCIAL00000001",
- "provider": "anthropic",
- "createdAt": "2026-07-10T10:00:00.000Z",
- "updatedAt": "2026-07-10T10:00:00.000Z"
}
]Upsert por (usuário, provider). A chave é cifrada por envelope encryption antes de tocar o banco, e a resposta traz só a PROJEÇÃO — nunca o segredo, nem cifrado.
| provider required | string Valores: "anthropic" "openai" "openrouter" "nvidia-nim" "together" "deepinfra" "bitdeer" "vultr" "9router" "opencode-zen" |
| apiKey required | string <= 512 characters A chave em texto puro. É cifrada por envelope encryption antes de tocar o banco e NUNCA volta em resposta nenhuma — nem cifrada. O teto de 512 caracteres é proteção contra payload absurdo, não validação de formato: se a chave presta quem responde é o provider, em |
{- "provider": "anthropic",
- "apiKey": "sk-ant-api03-…"
}{- "id": "01JC4Z0000CREDENCIAL00000001",
- "provider": "anthropic",
- "createdAt": "2026-07-10T10:00:00.000Z",
- "updatedAt": "2026-07-10T10:00:00.000Z"
}A checagem é uma ação explícita, separada do registro (ADR 0050): a api decifra o segredo, chama o provider e devolve SÓ o veredito — a chave nunca volta, nem em parte. Responde 200 para os três desfechos, porque o pedido foi processado; refused é um desfecho, não erro de protocolo. 404 quando não há credencial para (usuário, provider) — aí não há o que testar.
| provider required | string Exemplo: openrouter |
{- "resultado": "recusado",
- "motivo": "connection test failed for openrouter: openrouter responded 401"
}Caminho próprio, não o de users/me/credentials, porque o corpo é diferente (token, e o enum de provider é github/gitlab). O token é cifrado e guardado sem verificação prévia (ADR 0050); para verificá-lo, POST /users/me/credentials/{provider}/test. Leitura, remoção e teste reusam as rotas de users/me/credentials — é a mesma tabela.
| provider required | string Valores: "github" "gitlab" |
| token required | string <= 512 characters Personal access token. Cifrado por envelope encryption antes de tocar o banco e nunca devolvido, nem cifrado. O registro NÃO testa o token (ADR 0050) — para verificá-lo use |
{- "provider": "github",
- "token": "ghp_…"
}{- "id": "01JC4Z0000CREDENCIAL00000001",
- "provider": "anthropic",
- "createdAt": "2026-07-10T10:00:00.000Z",
- "updatedAt": "2026-07-10T10:00:00.000Z"
}Por conta, sem projeto no caminho (RN-611): a instalação de uma linha cria uma chave de máquina antes de existir qualquer projeto, e toda outra listagem é por projeto. Devolve SÓ chaves de máquina (especie: "maquina", projectId: null) e SÓ as do chamador — chaves de projeto ficam na listagem do projeto delas, e ninguém vê chave de outro usuário (RN-519). Chaves revogadas ENTRAM: registrar uma chave de máquina nova revoga a anterior (RN-552), e é nesta lista que isso aparece. lastUsedAt é uso registrado, nunca conexão viva; nulo quer dizer que a chave nunca foi usada.
[- {
- "id": "01JC4Z0000CHAVE000000000001",
- "name": "laptop",
- "projectId": "01JC4Z0000PROJETO000000001",
- "especie": "projeto",
- "createdAt": "2026-08-27T12:00:00.000Z",
- "revokedAt": null,
- "lastUsedAt": null
}
]Idempotente — revogar de novo não é erro. Mesma revogação de DELETE /projects/{projectId}/runner-device-keys/{deviceKeyId}: também derruba as conexões VIVAS abertas com esta chave, em TODO projeto (RN-520/RN-543). Desde a RN-685 (ADR 0201) o alvo é a CHAVE: outro runner do mesmo usuário, conectado com PAT ou com outra chave, fica de pé. Os tickets da chave ainda não usados são anulados. Sem projeto ainda, não há conexão a derrubar. Chave de PROJETO, chave que não existe e chave de outro usuário respondem o mesmo 404.
| deviceKeyId required | string |
{- "locale": "pt-BR",
- "responseLanguage": "automatico",
- "detectedLanguage": null,
- "detectedLanguageConfirmedAt": "2019-08-24T14:15:22Z",
- "effectiveResponseLanguage": {
- "language": "pt-BR",
- "origin": "interface"
}
}Os dois campos são opcionais e independentes — mandar um nunca toca o outro, e corpo sem nenhum é 400. locale (a interface) continua fechado à lista pt-BR/en. responseLanguage (RN-618) é automatico ou qualquer código BCP-47 reconhecido, gravado na forma canônica; código não reconhecido é 400 e nada do corpo é gravado.
| locale | string Valores: "pt-BR" "en" "ru" "zh-Hans" O idioma da interface. Fechado à lista — não é BCP-47 livre. |
| responseLanguage | string <= 35 characters O idioma em que os agentes respondem (RN-618): |
{- "locale": "en",
- "responseLanguage": "es"
}{- "locale": "pt-BR",
- "responseLanguage": "automatico",
- "detectedLanguage": null,
- "detectedLanguageConfirmedAt": "2019-08-24T14:15:22Z",
- "effectiveResponseLanguage": {
- "language": "pt-BR",
- "origin": "interface"
}
}O único jeito de um idioma detectado virar preferência (RN-624): a detecção sozinha nunca muda nada. confirm grava detectedLanguage + detectedLanguageConfirmedAt; decline registra a recusa para o mesmo idioma não ser perguntado de novo. Devolve as preferências depois da resposta.
| language required | string <= 35 characters O idioma sobre o qual era a pergunta — o |
| answer required | string Valores: "confirm" "decline"
|
{- "language": "es",
- "answer": "confirm"
}{- "locale": "pt-BR",
- "responseLanguage": "automatico",
- "detectedLanguage": null,
- "detectedLanguageConfirmedAt": "2019-08-24T14:15:22Z",
- "effectiveResponseLanguage": {
- "language": "pt-BR",
- "origin": "interface"
}
}Já filtrada por associação — não há listagem global. Cada item traz o papel do próprio chamador.
[- {
- "workspace": {
- "id": "01JC4Z0000WORKSPACE00000001",
- "name": "Acme Corp",
- "slug": "acme-corp",
- "createdBy": "01JC4Z0000USUARIO0000000001",
- "toolRouterEnabled": true,
- "createdAt": "2026-07-20T09:12:00.000Z",
- "updatedAt": "2026-07-20T09:12:00.000Z"
}, - "role": "maintainer"
}
]Não exige papel: quem cria vira owner. É a única porta de entrada para quem ainda não pertence a workspace nenhum.
| name required | string >= 2 characters |
| slug required | string^[a-z0-9]+(-[a-z0-9]+)*$ kebab-case. Único no sistema e visível na URL. |
{- "name": "Acme Corp",
- "slug": "acme-corp"
}{- "id": "01JC4Z0000WORKSPACE00000001",
- "name": "Acme Corp",
- "slug": "acme-corp",
- "createdBy": "01JC4Z0000USUARIO0000000001",
- "toolRouterEnabled": true,
- "createdAt": "2026-07-20T09:12:00.000Z",
- "updatedAt": "2026-07-20T09:12:00.000Z"
}| workspaceId required | string |
{- "id": "01JC4Z0000WORKSPACE00000001",
- "name": "Acme Corp",
- "slug": "acme-corp",
- "createdBy": "01JC4Z0000USUARIO0000000001",
- "toolRouterEnabled": true,
- "createdAt": "2026-07-20T09:12:00.000Z",
- "updatedAt": "2026-07-20T09:12:00.000Z"
}| workspaceId required | string |
| name | string >= 2 characters |
| slug | string^[a-z0-9]+(-[a-z0-9]+)*$ kebab-case. Único no sistema e visível na URL. |
{- "name": "Acme Corp",
- "slug": "acme-corp"
}{- "id": "01JC4Z0000WORKSPACE00000001",
- "name": "Acme Corp",
- "slug": "acme-corp",
- "createdBy": "01JC4Z0000USUARIO0000000001",
- "toolRouterEnabled": true,
- "createdAt": "2026-07-20T09:12:00.000Z",
- "updatedAt": "2026-07-20T09:12:00.000Z"
}Exige owner. Leva junto os projetos, as sessões e o histórico deles.
| workspaceId required | string |
{- "id": "01JC4Z0000WORKSPACE00000001",
- "name": "Acme Corp",
- "slug": "acme-corp",
- "createdBy": "01JC4Z0000USUARIO0000000001",
- "toolRouterEnabled": true,
- "createdAt": "2026-07-20T09:12:00.000Z",
- "updatedAt": "2026-07-20T09:12:00.000Z"
}Quem está associado ao WORKSPACE, com o papel de workspace — o papel que todo projeto herda a menos que uma linha de projeto o sobreponha. Completa GET projects/:projectId/members, que lista só linhas de projeto: quem alcança um projeto só pelo workspace aparece aqui e não lá. Só id, nome, e-mail e papel.
| workspaceId required | string |
[- {
- "userId": "01JC4Z0000USUARIO0000000002",
- "role": "maintainer",
- "name": "Senior Dev",
- "email": "dev@brabo.dev"
}
]Só owner mexe na lista de membros. O papel aqui é herdado por TODOS os projetos do workspace. Esse papel é NECESSÁRIO mas não SUFICIENTE: mudar o SEU PRÓPRIO papel aqui é recusado com 403 nos dois sentidos, e não pode ser habilitado em lugar nenhum. Não há nível acima para segurar a queda, então um autorrebaixamento seria irrecuperável pela interface. Rebaixar OUTRO owner continua permitido — é o único jeito de revogar propriedade.
| workspaceId required | string |
| userId required | string <uuid> Id do usuário a associar. |
| role required | string Valores: "viewer" "developer" "maintainer" "owner" Papel nesta associação. Num PROJETO ele SOBREPÕE o papel de workspace nos dois sentidos — o papel efetivo é este sempre que a associação existe, maior OU menor — e dois movimentos são recusados com 403: rebaixar um |
{- "userId": "3f1b2c8e-5a4d-4b7e-9c10-2d6f8a1b4c33",
- "role": "developer"
}{- "workspaceId": "01JC4Z0000WORKSPACE00000001",
- "userId": "01JC4Z0000USUARIO0000000002",
- "role": "maintainer",
- "createdAt": "2026-07-22T08:00:00.000Z"
}Só owner, como o upsert. Remove a linha do workspace E, na mesma transação, as linhas do usuário em todo projeto deste workspace (o papel de projeto sobrepõe o de workspace, então deixá-las manteria o usuário dentro desses projetos), mais as chaves de dispositivo de PROJETO e os personal access tokens do usuário neles. Depois a conexão de runner viva desse usuário em cada projeto é derrubada, em melhor esforço. Chaves de dispositivo de máquina e sessões abertas ficam como estão — deixam de alcançar este workspace porque o papel resolve para nenhum.
Remover a SI MESMO é recusado com 403 e não pode ser habilitado em lugar nenhum: não há nível acima para segurar a queda. Essa mesma cláusula é o que protege o último owner — só um owner chama esta rota e ninguém se remove, então toda chamada bem-sucedida deixa ao menos quem chamou como owner. Remover OUTRO owner é permitido — é como se revoga propriedade —, exceto o TITULAR (createdBy), recusado com 409 criador_do_workspace até PUT :workspaceId/owner-of-record passá-lo a outro owner: a chave de LLM que os agentes gastam e o relatório de gasto são dele. Idempotente: alvo sem linha de workspace ainda tem as linhas de projeto deste workspace limpas, e recebe 204.
| workspaceId required | string |
| userId required | string |
O titular é createdBy: de quem é a credencial de LLM que os agentes gastam e a credencial de git que as ações de agente usam (RN-058), e de quem é o relatório de gasto (RN-060). Não concede autorização — quem concede são os papéis. O alvo já tem de ser owner do workspace (senão 409 titular_precisa_ser_owner). A partir do commit, os turnos de agente buscam a credencial do NOVO titular; se ele não tem uma para o provider do modelo, o turno termina com o desfecho de sempre de "nenhuma credencial cadastrada" para aquele provider — esta rota não confere isso antes. Qualquer owner pode chamá-la, inclusive para outro owner. Transferir para o titular atual não faz nada.
| workspaceId required | string |
| userId required | string <uuid> Id do usuário que vira o titular. Tem de já ser |
{- "userId": "3f1b2c8e-5a4d-4b7e-9c10-2d6f8a1b4c33"
}{- "id": "01JC4Z0000WORKSPACE00000001",
- "name": "Acme Corp",
- "slug": "acme-corp",
- "createdBy": "01JC4Z0000USUARIO0000000001",
- "toolRouterEnabled": true,
- "createdAt": "2026-07-20T09:12:00.000Z",
- "updatedAt": "2026-07-20T09:12:00.000Z"
}O navegador de pastas do modo Mounted, servido pela api em vez do runner (RN-504). Escopado RIGIDAMENTE a BRABO_PROJECTS_BASE (ADR 0141): path é opcional e tem como padrão a própria base, e qualquer path fora dela é 400 — pedido malformado, não problema de permissão, já que papel nenhum navega fora da base.
Com teto de propósito: só diretórios, no máximo 500 (ordenados ANTES do corte, com truncado dizendo isso), nunca recursivo, entradas com ponto no início excluídas, e symlinks relatados mas nunca seguidos. O que fica de fora é CONTADO (arquivos, simbolicos) para uma pasta cheia de código nunca parecer vazia.
Não há POST irmão: criar pasta é parte de materializar o workspace montado, nunca do navegador.
| path | string Caminho absoluto a listar. Omitido quer dizer a própria base. Tem de estar dentro de |
{- "base": "/home/voce/brabo",
- "path": "/home/voce/brabo/clientes",
- "entries": [
- "api",
- "loja",
- "website"
], - "truncado": false,
- "arquivos": 12,
- "simbolicos": 1
}| workspaceId required | string |
[- {
- "id": "01JC4Z0000PROJETO0000000001",
- "workspaceId": "01JC4Z0000WORKSPACE00000001",
- "name": "Checkout",
- "slug": "checkout",
- "workspaceDirName": "checkout-3f2b1c8e",
- "executionMode": "container",
- "workspacePath": null,
- "workspaceVerifiedAt": null,
- "mirrorPath": null,
- "language": "pt-BR",
- "createdBy": "01JC4Z0000USUARIO0000000001",
- "taskBudgetMicros": 500000,
- "maxConsecutiveBlocked": 3,
- "storyPromotion": "auto",
- "createdAt": "2026-07-21T11:00:00.000Z",
- "updatedAt": "2026-07-21T11:00:00.000Z"
}
]| workspaceId required | string |
| name required | string >= 2 characters |
| slug required | string^[a-z0-9]+(-[a-z0-9]+)*$ kebab-case. Único dentro do workspace. |
| maxConsecutiveBlocked | number Circuit breaker (Fase 12b — RN-047): quantas tarefas consecutivas terminando bloqueadas param o dev agent do módulo em idle_tripped. Omitido usa o padrão do domínio (DEFAULT_MAX_CONSECUTIVE_BLOCKED). Vale da PRÓXIMA ativação de execução em diante — não afeta agentes já rodando. |
| storyPromotion | string Valores: "manual" "auto" Quem promove uma história de |
| executionMode | string Valores: "container" "mounted" "runner" ONDE o comando deste projeto EXECUTA (RN-169/RN-421 — ADR 0072/0104). |
| workspacePath | string O caminho ABSOLUTO da pasta, obrigatório quando |
| language | string <= 35 characters O idioma do PROJETO (RN-619) — artefatos compartilhados e turnos sem autor humano o usam. Qualquer código BCP-47 que o servidor reconhece, gravado na forma canônica; um não reconhecido é 400. Omitido, o projeto começa com o idioma de resposta efetivo de quem o criou (RN-618: escolha da conta > detecção confirmada > idioma da interface). |
{- "name": "Checkout",
- "slug": "checkout",
- "maxConsecutiveBlocked": 3,
- "storyPromotion": "auto",
- "executionMode": "container",
- "workspacePath": "/home/you/projects/store",
- "language": "pt-BR"
}{- "id": "01JC4Z0000PROJETO0000000001",
- "workspaceId": "01JC4Z0000WORKSPACE00000001",
- "name": "Checkout",
- "slug": "checkout",
- "workspaceDirName": "checkout-3f2b1c8e",
- "executionMode": "container",
- "workspacePath": null,
- "workspaceVerifiedAt": null,
- "mirrorPath": null,
- "language": "pt-BR",
- "createdBy": "01JC4Z0000USUARIO0000000001",
- "taskBudgetMicros": 500000,
- "maxConsecutiveBlocked": 3,
- "storyPromotion": "auto",
- "createdAt": "2026-07-21T11:00:00.000Z",
- "updatedAt": "2026-07-21T11:00:00.000Z"
}A única pasta da máquina do operador que os containers da api e do engine enxergam, montada por identidade (ADR 0141). null — estado normal, nunca erro — quer dizer que esta instalação não tem BRABO_PROJECTS_BASE, então o assistente de projeto não deve oferecer o modo Mounted de jeito nenhum. O mesmo valor para todo workspace: é configuração da instalação, e workspaceId só escopa a autorização.
brokerConfigurado responde a outra metade da mesma pergunta: container e mounted só sobem container pelo broker (ADR 0144), então sem ele o assistente não pré-seleciona Mounted e oferece Runner no lugar (ADR 0161, RN-573).
{- "projectsBase": "/home/voce/brabo",
- "brokerConfigurado": false
}Uma linha por projeto do workspace com tarefa bloqueada — alimenta o ponto de status da barra lateral do painel com uma consulta só em vez de N chamadas ao backlog.
| workspaceId required | string |
[- {
- "projectId": "01JC4Z0000PROJETO0000000001",
- "blockedTaskCount": 2
}
]Uma linha por projeto do workspace com provider de git, status de provisionamento, orçamento, última atividade, histórias aguardando promoção e os fatos que decidem a lista de agentes (RN-090).
Mesma ideia de projects-status, um andar acima: a grade de cards é uma leitura de WORKSPACE, não N leituras de projeto. O caminho anterior fazia sete consultas com poll por card, e com 23 projetos só o painel estourava o rate limit de 300 req/min.
| workspaceId required | string |
[- {
- "projectId": "01JC4Z0000PROJETO0000000001",
- "provider": "github",
- "provisioningStatus": "provisioned",
- "budget": {
- "limitMicros": 50000000,
- "spentMicros": 12500000
}, - "latestSessionId": "01JC4Z0000SESSAO00000000001",
- "latestSeq": 41,
- "lastEvent": {
- "id": "01JC4Z8QK3M7YV2N5T9B0PXHRB",
- "sessionId": "01JC4Z8QK3M7YV2N5T9B0PXHRA",
- "seq": 41,
- "type": "chat.message",
- "actor": {
- "kind": "agent",
- "id": "dev-api"
}, - "payload": {
- "role": "assistant",
- "content": "I'll open the epic first."
}, - "createdAt": "2026-07-27T14:30:58.512Z"
}, - "storiesAwaitingPromotion": 2,
- "pendingApprovalsCount": 8,
- "onlineAgentCount": 2,
- "roster": {
- "executionActivated": true,
- "moduleNames": [
- "api",
- "web"
], - "gatesEverOpened": true,
- "delegatedSubagents": [
- "qa-automacao"
], - "activatedAgents": [
- "arquiteto",
- "infra"
], - "infraActive": false,
- "uxDesignerActive": false,
- "staffActive": false
}
}
]Contagem de projetos, agentes que gastaram tokens neste mês e o gasto do mês — alimenta a linha de resumo do painel de projetos.
| workspaceId required | string |
{- "activeProjects": 4,
- "agentCount": 6,
- "spentMicros": 12500000
}Ligado por padrão. Quando ligado E o modelo do turno é do OpenRouter, um passo de agente com duas ou mais ferramentas primeiro pergunta ao Jev qual ferramenta cabe, e ao modelo de chat só são oferecidas essa ferramenta e a anterior. O Jev nunca aprova nem nega nada: ação que exige aprovação continua exigindo. Qualquer falha do Jev cai no catálogo inteiro. Desligá-lo para toda chamada ao Jev, e o custo delas junto.
| workspaceId required | string |
| enabled required | boolean Se o Jev escolhe a ferramenta de cada passo de agente. Ligado por padrão; só age quando o modelo do turno é do OpenRouter. |
{- "enabled": false
}{- "id": "01JC4Z0000WORKSPACE00000001",
- "name": "Acme Corp",
- "slug": "acme-corp",
- "createdBy": "01JC4Z0000USUARIO0000000001",
- "toolRouterEnabled": true,
- "createdAt": "2026-07-20T09:12:00.000Z",
- "updatedAt": "2026-07-20T09:12:00.000Z"
}A gaveta do sino, para o workspace inteiro (RN-091). Cada cursor diz até qual seq aquele projeto já foi lido, e a resposta traz o que veio depois disso na sessão mais recente de cada um.
É POST porque é o único verbo com CORPO, não porque muda estado. O servidor não sabe onde cada leitor parou: "lido até aqui" é um seq por projeto guardado no navegador, e (de propósito) não há endpoint de marcar como lido. Então o corte precisa VIAJAR no pedido, e são dezenas de pares — numa query string isso vira uma URL longa, que um proxy corta, e ainda poria os ids de projeto do usuário em logs de acesso. A rota é idempotente e não tem efeito colateral: responde 200, nunca 201.
Substitui um pedido POR PROJETO com a gaveta aberta — 23 projetos custavam 286 req/min contra um limite de 300.
| workspaceId required | string |
required | Lista de objects (UnreadCursorDto) <= 200 items Um cursor por projeto a consultar. Lista VAZIA devolve lista vazia — "não pedi nada" não é "me dê tudo". Projeto de outro workspace é ignorado em silêncio, porque o cursor vem do armazenamento local de quem chama e pode trazer sobras de um workspace antigo. | ||||
Array (<= 200 items)
| |||||
{- "cursors": [
- {
- "projectId": "01JC4Z0000PROJETO0000000001",
- "afterSeq": 41
}
]
}[- {
- "projectId": "01JC4Z0000PROJETO0000000001",
- "sessionId": "01JC4Z0000SESSAO00000000001",
- "events": [
- {
- "id": "01JC4Z8QK3M7YV2N5T9B0PXHRB",
- "sessionId": "01JC4Z8QK3M7YV2N5T9B0PXHRA",
- "seq": 41,
- "type": "chat.message",
- "actor": {
- "kind": "agent",
- "id": "dev-api"
}, - "payload": {
- "role": "assistant",
- "content": "I'll open the epic first."
}, - "createdAt": "2026-07-27T14:30:58.512Z"
}
]
}
]