Download OpenAPI specification:Download
The Brabo API. Every endpoint returns JSON, except the four explicitly
documented with a different content-type (two SSE ones, the metrics one,
and the OAuth callback, which redirects).
The access token is a 15-minute EdDSA JWT, presented in
Authorization: Bearer <token>. The public key lives at
GET /.well-known/jwks.json.
The refresh is not a JWT: it's an opaque token that lives in an
httpOnly cookie (brabo_refresh, Path=/auth, SameSite=Strict) and
never appears in any response body. POST /auth/refresh and
POST /auth/logout read the cookie and require the X-CSRF-Token header
to match the brabo_csrf cookie.
Rotation is mandatory. Each refresh consumes the presented token and issues another. Re-presenting an already-consumed one is a theft signature and revokes the WHOLE family — including the legitimate session. A client that fires two concurrent refreshes takes itself down; use a single in-flight call.
The /internal/* routes are the api ↔ engine surface and sit outside the
JWT: they require the X-Brabo-Service-Token header with the shared
secret. A user token doesn't open any of them.
NestJS default format: { "statusCode": number, "message": string | string[], "error": string }.
| code | when |
|---|---|
400 |
invalid body — the ValidationPipe runs with whitelist and forbidNonWhitelisted, so an unknown field also fails |
401 |
no token, expired token, or invalid credential |
403 |
authenticated but without the role RBAC requires, or a wrong service token |
404 |
resource doesn't exist or is invisible to the caller |
409 |
state conflict — invalid session transition, resource already exists |
429 |
rate limit or progressive lockout |
Auth routes respond deliberately indistinguishably between a nonexistent email, a wrong password, and a locked account: same body, same status, same timing. That's what closes account enumeration, and it's not a diagnostic defect.
Sliding window in Postgres: 300 requests per user and 600 per IP every 60s,
configurable. A @Public() route and a service route are exempt — what
guards /auth/* is the progressive lockout, not the rate limit.
A nonexistent email, wrong password, and a locked account all return exactly the same 401 response — same body, same status.
| email required | string |
| senha required | string |
{- "email": "fulano@brabo.dev",
- "senha": "correct horse battery staple"
}{- "accessToken": "string",
- "expiresIn": 900,
- "locale": "pt-BR"
}Success goes to WEB_ORIGIN/ already with the session cookies set; failure goes to WEB_ORIGIN/login?oauth_error=1, without detailing the reason.
| provider required | string Enum: "github" "gitlab" |
| code required | string |
| state required | string |
The presented refresh is consumed. Re-presenting an already-rotated token revokes the WHOLE family and records a security event — even when the cause was a client double-submit.
{- "accessToken": "string",
- "expiresIn": 900,
- "locale": "pt-BR"
}Responds 202 for both a new address and an already-registered one — the response doesn't reveal whether the account exists. In the second case nothing is created and the address's owner gets a notice.
| email required | string The account's email. |
| senha required | string >= 12 characters Password. Minimum of 12 characters; no requirement for uppercase, digit, or symbol. |
| 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."
}Responds 202 for both a known and an unknown address. It is also the path for whoever was imported from Keycloak and hasn't set a password yet.
| email required | string |
{- "email": "fulano@brabo.dev"
}{- "message": "If the address is available, we've sent an email."
}Revokes ALL of the user's sessions. Does not issue tokens: whoever resets the password is sent to login, so that compromising the email doesn't equal taking over the account in a single step.
| token required | string Single-use token received by email. |
| novaSenha required | string >= 12 characters |
{- "token": "string",
- "novaSenha": "another pretty long passphrase"
}Declarative index of the flow gates (ADR 0054). The screen derives from here which steps exist and who judges each one, instead of repeating the list in code — which is how it used to go stale unnoticed.
{- "version": 1,
- "gates": [
- {
- "id": "qa-verificada",
- "fluxo": "pr",
- "dono": "area-qa",
- "entrada": [
- "pr-aberta"
], - "entregavel": "veredito-qa",
- "aprovacaoHumana": true,
- "severidade": "block"
}
]
}A gate cycle stalled for more than 2 h is parked instead of restarted on its own (ADR 0207). This is the human gesture that resumes it.
| projectId required | string |
| taskId required | string |
| gate required | string |
{- "ok": true
}Public because whoever arrives is the user's BROWSER coming from the provider, without an api session. It isn't unrestricted: the state is verified by HMAC and without a valid one the call is refused. Never responds with JSON — always redirects, because a raw error body in a browser navigation would be a terrible experience.
| provider required | string Enum: "github" "gitlab" |
| code required | string |
| state required | string |
Foundation for the declared blame pending item (PHASE 26b) — the UI that annotates the editor line by line doesn't exist yet. truncated warns when the file went past the annotated-lines cap.
| projectId required | string |
| path required | string Path of the file to annotate. |
| ref | string Branch, tag, or sha. Absent uses the repository's default branch. |
{- "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
}Foundation for the rich dropdown (PHASE 26b) — today the ref selector is a plain text field. ahead/behind are relative to the repository's DEFAULT branch; null when the provider can't compute it (an honest degradation, not an error). It is NOT listBranches (which the Gitflow bootstrap uses) — it is its own operation, because enriching costs an extra call to the provider PER 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, cut at the byte cap with truncated: true when it's exceeded. A path that escapes the project folder is refused with 400 by the central check in RN-095 — never with 404, which would invite probing.
| projectId required | string |
| path required | string File path. |
| ref | string Branch, tag, or sha. |
{- "ref": "dev",
- "path": "apps/api/src/main.ts",
- "content": "import { NestFactory } from '@nestjs/core';\n",
- "truncated": false,
- "bytes": 1284
}Foundation for the navigable list (PHASE 26b) — today the diff is only reachable by a known id (e.g. coming from Approvals); this route resolves how to reach the id without having to know it by heart. truncated warns when the list went past the per-call cap.
| projectId required | string |
| state | string Enum: "open" "merged" "closed" Absent lists all states. |
{- "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
}A null patch distinguishes the case where the provider didn't deliver the text (binary, or a patch too large) from the case where the text came back empty. Collapsing the two would make the screen say a changed binary didn't change. The truncated field warns when the file list went past the cap.
| 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
}Search is NOT a git contract operation: it is composed over the tree and the content, and so is the only read whose cost grows with the size of the repository. Three budgets cap it — directories walked, files opened, and matches returned — and truncated says it stopped. filesScanned shows the cost it incurred.
| projectId required | string |
| q required | string Term, 2 to 200 characters. |
| ref | string Branch, tag, or sha. |
| path | string Subtree to search in. Narrowing this is what avoids truncation. |
{- "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
}Not recursive, by contract design: the tab navigates on demand, and requesting the whole tree of a large repository is exactly the traffic amplifier this phase forbids. truncated warns when the level went past the entry cap.
| projectId required | string |
| ref | string Branch, tag, or sha. Absent uses the repository's default branch. |
| path | string Directory to list. Absent or empty is the root. |
{- "ref": "dev",
- "path": "apps/api",
- "entries": [
- {
- "path": "apps/api/src/main.ts",
- "name": "main.ts",
- "type": "file",
- "size": 1284
}
], - "truncated": false
}Doesn't redirect: returns the URL for the client to decide when to send the browser. The state goes signed by HMAC, and that is what prevents the callback from being forged.
| projectId required | string |
| provider required | string Enum: "github" "gitlab" |
{
}SYNCHRONOUS: the response only comes back after the whole bootstrap (permanent branches, protections, templates) has run — there is no worker and no queue behind this. It is IDEMPOTENT AND RESUMABLE, so calling it again after a failure resumes from the step that failed. Track progress and read the failure reason via GET /projects/:id/git/bootstrap.
| projectId required | string |
| provider required | string Enum: "local" "github" "gitlab" |
| name required | string Repository name on the provider. |
| visibility required | string Enum: "public" "private" |
| namespace | string Organization or group. When omitted, the repository is created in whoever registered the credential's personal account. |
{- "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"
}
}Validates access with getRepo and produces a PLAN (dry-run) of what the bootstrap would do — missing branches, missing protections, files. NOTHING is executed on the repository: decide afterward via plan/approve or plan/skip. Re-adopting the same repository converges (regenerates the plan without duplicating a row).
| projectId required | string |
| provider required | string Enum: "local" "github" "gitlab" |
| externalId required | string The repository's identifier ON THE 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
}Which step it is on, whether it failed and where, and how many attempts there were. The step-by-step detail comes from the bootstrap.step_* events of the dedicated session.
| projectId required | string |
{- "status": "provisioning",
- "sessionId": "01JC4Z8QK3M7YV2N5T9B0PXHRA",
- "failedStep": null,
- "lastError": null,
- "attempts": 1
}protect_branches fails on a private repository on the free plan, and the wizard warns about this BEFOREHAND. Without this exit the only button was "Try again", which always fails for the same reason — and provision_failed makes the dashboard redirect the project back to the provisioning page, leaving it unreachable forever. Only a failure in PROTECTING can be acknowledged: it is the last step and the only one whose failure leaves a usable repository. The product's merge lock (RN-006) doesn't depend on the provider's protection and keeps applying.
| projectId required | string |
{- "status": "provisioned"
}decision: null with a plan present is the state where NOTHING runs — waiting for approval or as-is adoption (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
}Approval is all-or-nothing: approving loose steps would break the dev←main, qa←dev, rc←qa cascade. What runs is the plan RE-DERIVED at execution time — equal to or smaller than what was shown, never larger.
| projectId required | string |
| planGeneratedAt required | string <date-time> The |
{- "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"
}
}Records the decision and doesn't touch the repository. The plan stays stored as evidence of what was deliberately not applied, and the bootstrap cursor is NOT tampered with — no step ran.
| projectId required | string |
| planGeneratedAt required | string <date-time> The |
{- "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"
}Runs a select 1. Reveals only whether the database responds; no topology or version. Public because the caller is kubelet, which carries no token.
{- "service": "api",
- "status": "ok",
- "timestamp": "2026-07-27T15:40:00.000Z",
- "details": { }
}Does NOT touch the database, on purpose. If it did, a slow Postgres would restart ALL replicas at once and turn degradation into a total outage. The pod is pulled from the load balancer by /health.
{- "service": "api",
- "status": "ok",
- "timestamp": "2026-07-27T15:40:00.000Z",
- "details": { }
}Not JSON: it's text/plain in exposition format. Public out of necessity (the scraper carries no token) and restricted by NETWORK — the production Ingress blocks this path at the edge and the NetworkPolicy only allows the monitoring namespace.
# HELP oban_queue_depth Oban queue depth # TYPE oban_queue_depth gauge oban_queue_depth{queue="default",state="available"} 0
GitHub Releases proxy — platform accepts only linux-x64, linux-arm64, darwin-arm64. No authentication: the binary is not a secret. The bytes are checked against the checksums.txt of the same Release before anything is answered (ADR 0149, RN-525); the route does NOT verify the manifest signature — that is integrity, not provenance, and whoever verifies the signature is install.sh.
For the one-line installer, which runs where no mail transport is configured: the generated .env carries no mail variable, MAIL_TRANSPORT falls to log, and normal registration waits on an e-mail that never arrives. The account is born verified because whoever runs the installer already proved something STRONGER than controlling a mailbox — they control the machine, the .env and the Docker daemon. Normal registration is untouched. Refuses with 409 when the installation has ANY user: the condition is about the installation, not about this e-mail, so a migration restore silences the step by the same test. The password is never generated here and never stored anywhere but as an argon2id hash; the personal workspace (RN-410) is born in the same transaction.
| email required | string The owner e-mail, typed at the TTY. Normalized before anything is written; it is what the installer records in its marker, since an e-mail identifies and is not a secret. |
| senha required | string Read without echo and confirmed by the installer. Used and discarded — never written to the |
| nome | string Display name. Omitted: the personal workspace's name falls back to the e-mail local-part (RN-410) — nothing is invented. |
{- "email": "voce@exemplo.dev",
- "senha": "uma frase longa e minha",
- "nome": "Fulana de Tal"
}{- "userId": "01JC4Z0000USUARIO0000000001",
- "email": "voce@exemplo.dev",
- "workspaceId": "01JC4Z0000WORKSPACE00000001"
}The registry DESCRIBES, it does not execute: no gate becomes enforced because of it, and changing a field here changes no behavior at all. Each gate says where the proof that it passed lives (evidencia), because not every proof is in the 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"
}
]
}If a version with the SAME hash already exists for the same name, no new version is created — the existing one is returned (the idempotency hit does not change the HTTP status: always 201, for simplicity — the body is identical to what would have been created).
| 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
}A specific version looks up by equality; omitted, it looks up the most recent active version. 404 if the template (or the requested version) does not exist.
| name required | string |
| version | string Example: version=3 |
{- "name": "dev-agent-kickoff",
- "version": "3",
- "body": "You are the dev agent for the {{modulo}} module...",
- "hash": "sha256:9f2c..."
}For the one-line installer, right after it creates the first account: the local agent needs a credential, and the only one that existed was bound to a PROJECT — in an installation that has none yet. The pair is generated ON THE MACHINE and only the public half arrives here; write the returned id into the private JWK as kid (RN-475), which is the only link between the file on disk and the public half on the server. There is no userId in the body ON PURPOSE: the owner is the installation's SOLE user, resolved by the api, so holding the service token never means choosing whose credential to mint. Registering REPLACES: the owner's active machine keys are revoked in the same transaction, so a reinstalled machine works and a thousand machine keys cannot exist. Project keys (ADR 0118) are never touched.
| name required | string A name for a human to recognize this MACHINE later, in the device key list of every project it serves. Not unique. The installer suggests the hostname; nothing here is derived from it by the api. |
| publicKeyJwk required | string The PUBLIC Ed25519 JWK (RFC 8037), serialized as JSON. The pair is generated on the machine and the private half never travels — a JWK carrying |
{- "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": [ ]
}Never fails because of one provider: each one answers for itself in the report, with the reason it was skipped and the origin of the failure when there was one. A provider that did not respond is SKIPPED, not marked unavailable.
{- "porProvider": [
- {
- "provider": "openai",
- "descobertos": 3,
- "reencontrados": 1,
- "indisponibilizados": 0,
- "pulado": "sem_capability",
- "origemDaFalha": "infra",
- "detalhe": "openai responded with status 401"
}
]
}The SAME epic → story → task tree as the Backlog tab, through the same use case (three reads per project, never N+1). It is with this that the PO sees an orphan epic and a story with no task instead of re-creating what already exists (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"
}
]
}
]
}
]Every artifact.business_rule from the project's sessions — not just the current session's, which was the ceiling of the PO's kickoff context — with the full description and which stories already cite each rule. uncoveredCount is the pending item: a rule no story covers (RN-164). A project with no rule at all responds 200 with an empty list: "no rule captured yet" is not an error.
| 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
}Called only by the engine, when Engine.Actions.TerminalExecutor decided the command belongs inside the real container (execution_mode: container with a REGISTERED running row). Proxies to ContainerBrokerPort.exec (ADR 0130/0134) — the last of the broker's five operations to gain a real caller. sucesso: false in the response — never a thrown error — is the NORMAL shape for a refused or unreachable broker: the container may have died or been removed from outside between the registered running row and this call (RN-486, registered and observed never merge), and that is a command failure, not a transport error.
| projectId required | string |
| comando required | string The shell command, interpreted by |
| cwd | string Working directory INSIDE the container. Already translated from the host path by the engine (RN-492) — this DTO doesn't repeat that translation. Omitted runs at |
| timeoutMs | number Timeout in milliseconds. Omitted uses the broker default. |
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"
}Project identity, execution mode and the Architect's current image decision. The broker revalidates all of it before handing anything to the daemon — reading from here is not the same as trusting it, and the refusal names the field. No ABSOLUTE path is returned: the bind source is resolved by the daemon against the HOST filesystem, and a path from inside the api container would silently mount an empty folder. What travels is localizacao (RN-503) — which of the two broker roots to use, plus the relative segment that root does not cover.
| projectId required | string |
| papel | string Value: "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": [
- { }
]
}
}Returns the clean origin (no credential embedded) and, for a remote provider, the workspace OWNER's token (RN-058) decrypted on the fly. Whoever consumes it injects the token per invocation and NEVER writes it to a file — .git/config sits inside the folder where RN-075 grants the dev agent auto-approved read access.
| projectId required | string |
{- "kind": "remote",
- "defaultBranch": "main",
- "token": "string",
- "username": "x-access-token"
}Called only by the engine, after the runner pushes mirror_sync_result over the channel — the SAME path as workspace_confirm (runner → channel → engine → api), never a second mechanism: the engine reports, it does not write the table. This is TELEMETRY, not a domain event: never session_events (a mirror round has no session and session_id is NOT NULL, the same reasoning that made rag_searches a table) and never a proposed_action (the mirror write is configuration the user declared, not an agent asking to act). ok is what decides the outcome — never the presence of a count or of a message: a round that copied 0 files is normal and must not be indistinguishable from one that never ran. Recording never breaks what it measures: the copy is already done when this is called, and the engine only logs a refusal here. A project whose destination was cleared meanwhile STILL records — the round happened, and its error is usually what explains what went wrong.
| projectId required | string |
| ok required | boolean The REAL outcome of the copy, reported by the local agent after it finished — never an optimistic "ok" before the round ends. |
| destination | string The destination the round actually resolved (success) or tried to use (failure), on the USER machine. FROZEN in the row: showing the project's current destination next to yesterday's copy would be the screen asserting about a folder that round never touched. Omitted when the failure is about the destination itself. |
| filesCopied | number Regular files copied in this round. |
| filesSkipped | number Entries git listed that are not regular files — a nested repository (a dev agent's worktree is one), a symlink, or a file that vanished between the listing and the copy. |
| filesRefused | number Targets the per-file guard refused (a symlink escaping the destination). Counted, never swallowed. |
| error | string The NAMED failure message. Stored truncated when very long, saying it was truncated. It never erases the last successful sync — which row is CURRENT is decided by comparing the two timestamps. |
{- "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"
}One entry per module of the CURRENT module_map: what it consumes (dependeDe, the map's dependsOn) and what it exposes (expoe, from the Architect's current artifact.module_contracts, or null when none was declared). It is what a dev agent reads instead of opening another module's worktree (ADR 0200). A project with no contract responds 200 with 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": [ ]
}The SAME report as the analise:funil script (ADR 0089) — session → commit → PR → merge funnel, real lead time and real deployment frequency — through the same pure functions and the same query (apps/api/src/application/services/funil-metrics.ts), so the two never diverge from the same fact. Closes docs/fluxo.yml (role po, input metricas-de-produto, previously 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"
}Called only by the engine, after a runner connects and sends workspace_confirm over the channel. The runner is the SOURCE OF TRUTH for the path — the api overwrites workspacePath with what it reported, after re-validating it lexically (system root/overlap with Brabo are still forbidden even coming from the runner). Idempotent: reconnecting with the SAME path writes nothing again.
| projectId required | string |
| path required | string Absolute path confirmed by the runner ON THE HOST — the source of truth (RN-423). Re-validated LEXICALLY here before writing; invalid is 400, never written. |
| sessionId | string <uuid> The project's most recent session ( |
| actorId | string <uuid> Who requested the runner ticket (owner of the socket that confirmed) — becomes the actor of the |
{- "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 | Array of 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"
}
]
}Same use case as the human route — no second judgement path. An unknown searchId/chunkId is a 400 that the engine turns into an error tool-result for the model to correct (RN-061), never a crash.
| projectId required | string <uuid> |
| searchId required | string <uuid> |
| chunkId required | string <uuid> |
| verdict required | string Enum: "util" "irrelevante" |
| agent required | string Agent slug — the voter. |
{- "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
}Same engine as POST /projects/:projectId/rag/search (human route), projected into the format the engine's rag_search tool expects.
| projectId required | string <uuid> |
| query required | string |
| topK | number |
| sessionId | string <uuid> The session the agent is running in. Absent means the caller had no session — the telemetry row is still written (RN-479), only the |
| agent | string Agent slug, recorded as the telemetry actor ( |
{- "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
}The ONLY door through which an agent touches git, terminal, or spend. permissions.json decides at creation, and deny beats any autonomy granted to the agent.
| sessionId required | string |
| projectId required | string <uuid> |
| actionType required | string Enum: "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 Always an agent on this route. |
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"
}Everything in one call, mirroring the Psychologist's context. Members already come EXCLUDING whoever opted out of being profiled, and the user's decisions within the window come through here because they aren't in the event log. The engine reads the events themselves straight from Postgres — carrying them over HTTP would be more expensive without being more correct.
| 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"
}Records an anamnese run with zero profiles so the next round starts at windowTo — without it, the window never advanced and every tick paid for a new round over the same 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": [
- { }
]
}The artifact IS the artifact.c4_diagram event: immutable, versioned, and with an author, alongside artifact.module_map. Requires a current module_map — without one there is no Container level to draw (400).
| sessionId required | string |
| projectId required | string <uuid> |
| systemName required | string System/project name, for the diagram's label. |
| systemDescription | string |
Array of objects (C4AtorInternalDto) External actors of the Context level (Simon Brown). The Container level's containers do NOT go here: they come from the project's current module_map. |
{- "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 (with the verdict), failed (with the origin), or dispensed (with the justification) — the lead never calls this route with a halfway outcome: every delegation is born here already resolved.
| sessionId required | string |
| projectId required | string <uuid> |
| area required | string The area that delegated — "qa" (Phase 8b) or "infra" (Phase 8c). Text, not enum. |
| taskId | string <uuid> The backlog task behind the delegation, when it exists. QA (Phase 8b) always sends it; Infra (Phase 8c) never does — the delegation is about the SESSION, not a task. |
| leadAgent required | string |
| subagent required | string |
| status required | string Enum: "completed" "failed" "dispensed" |
| parecerArtifactId | string Id of the |
| failureOrigin | string Enum: "infra" "modelo" "codigo" "politica" The ORIGIN of the failure (ADR 0020/0038), never by elimination — required when |
| failureReason | string |
| justification | string Why the lead chose NOT to delegate — required when |
{- "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"
}A single call with everything the prompt needs: the whole story, the resolved business rules, and the applicable ADRs. module restricts the ADRs to that dev's module; an ADR with no declared module is CROSS-CUTTING and always comes in. Omitting module brings the whole collection, which is what the QA and SecOps gates want when reusing this same context.
| projectId required | string |
| taskId required | string |
| module | string Example: 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"
}Used to REHYDRATE a conversational agent's history and to read the artifacts its kickoff needs. The equivalent human route is protected by RBAC; this one, by the service token. latest=true returns the TAIL (still in ascending seq) and ignores afterSeq; types restricts the page to those event types. The page is capped at 200 either way (ADR 0060).
| sessionId required | string |
| projectId required | string |
| afterSeq | string Example: afterSeq=40 |
| limit | string Example: limit=200 |
| latest | string Example: latest=true Fetches the tail of the log; ignores |
| types | string Example: types=artifact.product_brief,artifact.business_rule Comma-separated event types (at most 20). Only events of these types count toward |
{- "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
}Same use case and same atomic seq assignment as the human route. The Psychologist's hypotheses do NOT go through here: they have their own route, with evidence validation.
| sessionId required | string |
| projectId required | string <uuid> |
| type required | string See |
| actorKind required | string Enum: "user" "agent" "system" |
| actorId required | string Slug of the agent that emitted it. |
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"
}The response's nextAction is what the engine obeys: correct returns to dev, run_secops advances the gate, done releases it to the user, await_user parks the cycle until the open question is answered (RN-860, no correction counted), and blocked means the correction cap ran out. MERGE is never automatic — done only means it's the human's turn.
| sessionId required | string |
| projectId required | string <uuid> |
| taskId required | string <uuid> |
| gate required | string Enum: "qa" "secops" |
| veredito required | string Enum: "approved" "changes_requested" "needs_user_decision"
|
| resumo required | string |
| itens required | Array of strings What needs to change, item by item. |
| pergunta | string With |
| sugestao | string With |
| openQuestionId | string <uuid> With |
| maxCorrections | number Cap on round trips before the task becomes |
{- "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"
}Born as offered. Who accepts is a PERSON, via the human route — an agent doesn't activate an agent. Two exceptions. The Creative→PO offer carrying the product_brief that an "I'm ready — the need is validated" click asked for is accepted on behalf of whoever clicked, with that person as the actor and implicito in the payload (RN-658, ADR 0185). The PO's handoff to the Arquiteto is accepted by the SYSTEM when the backlog is covered (at least one business rule, none without a story) and the repository is local with no git credential in the project (RN-660, ADR 0186); aceiteAutomatico says whether it happened and, if not, why. Either way the response then carries status: accepted.
| sessionId required | string |
| projectId required | string <uuid> |
| fromAgent required | string |
| toAgent required | string |
| artifactId | string ULID of the session_event for the artifact that motivated the handoff. |
| seAusente | boolean Only if nobody got it yet (ADR 0182, RN-636): with a pending offer to the target in ANY session of the project, return it ( |
{- "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"
}
}Each hypothesis needs to cite events that EXIST in this session — made-up evidence is rejected with 400, and that is what separates a hypothesis from an opinion. The session's previous round becomes superseded.
| sessionId required | string |
| projectId required | string <uuid> |
| tier required | string Enum: "leve" "pesada" The two tiers use genuinely different models; the cost diverges. |
| triggeredBy required | string Enum: "auto" "manual" |
| eventCount required | number Size of the analyzed log. |
| cause | string Enum: "normal" "timeout" "kill" "crash" "node_shutdown" "unknown" Cause CLASSIFIED by the engine. It, not the terminal status, decides whether |
required | Array of 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"
}
]
}The content comes from the proposed_action's own payload: an infra artifact never touches a worktree, same as 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…"
}
]
}The current module_map plus the relevant infrastructure ADRs, plus the Architect's module routing (ADR 0131) — the candidates the Infra Lead elects among when proposing 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
}
}
}Same pipeline and same state machine as dev PRs.
| sessionId required | string |
| projectId required | string <uuid> |
| prActionId required | string <uuid> The |
| gate required | string Enum: "qa" "secops" |
| veredito required | string Enum: "approved" "changes_requested" |
| resumo required | string |
| itens required | Array of 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"
}
}Becomes a proposed_action, not a direct write: changing an agent's behavior is an external effect and goes through the same approval pipeline as everything else. This closes the hypothesis → patch loop.
| sessionId required | string |
| projectId required | string <uuid> |
| agent required | string |
| proposedContent required | string The WHOLE file as it would end up, not a diff. |
| rationale required | string |
| hypothesisId | string <uuid> The hypothesis that motivated the patch. This is what closes the Anamnesis loop. |
{- "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"
}Metering is MANDATORY: every turn records token_usage, and that is what makes the budget mean something. Doesn't record any event — the engine narrates the event log. A provider failure comes back in error with 200, because the accounting in usage remains valid: the turn spent even while failing.
| sessionId required | string |
| projectId required | string <uuid> |
| agentId | string Attributes the cost to this agent in the team panel. |
required | Array of objects History already assembled by the engine's ContextManager. |
Array of objects Tools offered to the model; | |
| catalogoCompleto | boolean Asks for the WHOLE tool catalog: the Jev tool router (ADR 0179) is skipped for this call. The engine sets it when it repeats a step whose restricted menu made the model answer without calling a tool. |
{- "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
}
}Same semantics as llm-turn, delivered frame by frame. The done frame carries the usage — without it the turn would have come out with no accounting.
| sessionId required | string |
| projectId required | string <uuid> |
| agentId | string |
required | Array of objects |
Array of objects | |
| catalogoCompleto | boolean Asks for the WHOLE tool catalog: the Jev tool router (ADR 0179) is skipped for this call. The engine sets it when it repeats a step whose restricted menu made the model answer without calling a tool. |
{- "projectId": "01JC4Z0000PROJETO0000000001",
- "agentId": "criativo",
- "messages": [
- {
- "role": "user",
- "content": "Summarize what we decided."
}
], - "tools": [
- { }
], - "catalogoCompleto": true
}Refuses to propose a cap equal to or lower than the current one: the Anamnesis runs periodically, and would re-propose the same thing every round, filling with noise a queue the user needs to read.
| sessionId required | string |
| projectId required | string <uuid> |
| area required | string The area whose cap would go up. |
| proposto required | number >= 1 The proposed cap. Has to be HIGHER than the current one — proposing the same or less turns into noise in a queue the user needs to read, and the Anamnesis runs periodically. |
| rationale required | string Anchored in OBSERVED DECISIONS, not an impression. |
{- "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"
}The artifact IS the artifact.module_contracts event: immutable, versioned, and with an author, alongside artifact.module_map. Each call carries the WHOLE list and replaces the previous version. What a module consumes is not written here: it is the current module_map's dependsOn.
| sessionId required | string |
| projectId required | string <uuid> |
required | Array of objects (ContratoDeModuloInternalDto) The WHOLE contract list: each call is a new version that replaces the previous one. What a module CONSUMES is not here — it is derived from the current module_map's |
{- "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
}The history is immutable: each publication is one more version, and the current one is the one with the highest version. A dependency CYCLE between modules gets the map REJECTED with 400 — the graph needs to be acyclic.
| sessionId required | string |
| projectId required | string <uuid> |
required | Array of objects Module graph. A CYCLE makes the map get rejected with 400 — the validation is domain-level. Each module may declare |
{- "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"
}The artifact IS the artifact.module_routing event: immutable, versioned, and with an author, alongside artifact.module_map/artifact.project_image/artifact.c4_diagram. Each item is validated with the same rule as choose_project_image (explicit tag/digest, latest rejected, non-trivial rationale); a module name outside the current module_map, an empty list, or a repeated module are all rejected with 400.
| sessionId required | string |
| projectId required | string <uuid> |
required | Array of objects (RoteamentoDeModuloInternalDto) One item per module of the current module_map. The candidate image is the Architect proposing, not deciding — Infra elects among the candidates in a later step. |
{- "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
}Queried by the engine's SessionServer on heartbeat timeout. pending: true makes the timeout get rescheduled instead of closing, and the motivo goes into the engine's log — a session that doesn't close without saying why is undiagnosable.
| sessionId required | string |
{- "pending": true,
- "motivo": "handoff po → arquiteto aguardando aceite",
- "aguardandoUsuarioDesde": "2026-09-18T12:00:00.000Z"
}A competency outside the catalog is REJECTED, and so is evidence that cites an event from another project — both validations exist because the model gets both things wrong.
| sessionId required | string |
| projectId required | string <uuid> |
| windowFrom required | string <date-time> |
| windowTo required | string <date-time> |
| eventCount required | number |
required | Array of 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": [
- { }
]
}The artifact IS the artifact.project_image event: immutable, versioned, and with an author. Revising means issuing a new version, never overwriting. While there isn't one yet, the project's container does not come up and the Code tab responds 409.
| sessionId required | string |
| projectId required | string <uuid> |
| image required | string OCI reference with an explicit TAG or digest. |
| rationale required | string Why this image. Minimum of 10 characters. |
| network | string Default: "none" Enum: "none" "egress" The container's network posture. |
object Resource cap. Above the maximum, the decision is REFUSED, never silently downgraded. |
{- "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 is what gives the automatic path IDEMPOTENCY: with true the worker short-circuits without spending a token. The prior hypotheses come along so the round doesn't repeat itself.
| 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
}
]
}Embeds the title and the titles of the project rules (the 100 most recent) with the RAG embedding model and warns at cosine ≥ the threshold. Never refuses. Without an embedding provider the check is SKIPPED with the reason, also narrated in the event log. The embedding spend is metered as its own token_usage row (actor system/duplicata-semantica).
| sessionId required | string |
| projectId required | string <uuid> |
| kind required | string Value: "business_rule" |
| title required | string Title of the rule JUST appended. Rules with the same normalized title (RN-080 makes it the new one) are left out of the comparison. |
{- "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 is what feeds the rule→story coverage. Each id has to reference an artifact.business_rule event that EXISTS — validation rejects a made-up id. The response also carries semanticDuplicate (RN-681): an embedding-based WARNING against the project stories, never a refusal.
| sessionId required | string |
| projectId required | string <uuid> |
| epicId required | string <uuid> |
| title required | string |
| description | string |
| rf | Array of strings |
| rnf | Array of strings |
| dod | Array of strings |
| dor | Array of strings |
| businessRuleIds | Array of strings Each id needs to reference an |
{- "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"
}
}The story and its tasks leave the backlog, coverage, plan and claim; the row stays and backlog.story_archived records it, with the PO as actor (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 is ADDED to the rules already linked; non-empty lists replace the current ones and omitted lists stay. After writing, the story is promoted by the SAME criterion as story creation (ready in auto mode, proposedReady in manual) (RN-720).
| sessionId required | string |
| storyId required | string |
| projectId required | string <uuid> |
| description | string |
| rf | Array of strings |
| rnf | Array of strings |
| dod | Array of strings |
| dor | Array of strings |
| businessRuleIds | Array of strings ADDED to the rules the story already cites (completing never unlinks a rule); each new id has to reference an existing |
| questionId | string <uuid> RN-859 (ADR 0214): an open question the user ALREADY answered — only then a |
{- "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"
}Only a draft story with no task in execution; writes backlog.story_updated with the PO as actor (RN-727). A ready one too when questionId cites an open question the user already answered (RN-859).
| storyId required | string |
| projectId required | string <uuid> |
| title | string |
| description | string |
| questionId | string <uuid> RN-859 (ADR 0214): an open question the user ALREADY answered — only then a |
{- "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"
}This is the cross-validation: a module that doesn't exist in the current map becomes an architecture pending item instead of slipping through unnoticed.
| sessionId required | string |
| projectId required | string <uuid> |
| storyId required | string <uuid> |
| moduleIds required | Array of strings Names from the current |
{- "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"
}There is no automatic unblocking: whoever unblocks it is a person, via the human route. That is what stops an agent from spinning indefinitely.
| sessionId required | string |
| taskId required | string |
| projectId required | string <uuid> |
| agentId required | string |
| reason required | string |
| diagnosis required | string Free-text diagnosis — a human detail of what happened, not the structured origin of the failure. |
| origin | string Enum: "infra" "modelo" "codigo" "politica" The ORIGIN of the failure: |
{- "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"
}Called when the dev agent's PR is ready for review.
| 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 Only the agent that claimed the task can move it. |
| status required | string Enum: "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"
}Called by the engine retry job (up to 3 times, after 1, 3 and 10 min). Same use case as the human unblock, with a system actor; a task that is no longer blocked is left untouched.
| sessionId required | string |
| taskId required | string |
| projectId required | string <uuid> |
| agentId required | string The dev agent whose transient failure scheduled the retry. |
{- "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"
}ATOMIC by design: with several dev agents on the same module, two concurrent claims cannot return the same task. With no task available, returns empty instead of an error.
| sessionId required | string |
| projectId required | string <uuid> |
| module required | string Only tasks from this module enter the draw. |
| 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"
}Not blocked and outside the PR gates; empty unless the session is an execution session. Used by the engine when stopping an execution, to block the ones no live dev agent holds.
| sessionId required | string |
[- {
- "projectId": "5a8591dd-4039-49df-9202-96385ba3eff8",
- "taskId": "e6e9d88a-9b63-468a-aec3-b7a11de27af8",
- "assignedTo": "dev-api"
}
]Only terminations the api did NOT cause reach here — crash, kill, heartbeat_timeout, defensive shutdown. A stop planned by the api itself doesn't go through here: the engine already learned of it via the outbox, and reporting it back would be an echo.
| sessionId required | string |
| projectId required | string <uuid> |
| to required | string Enum: "closing" "closed" "closed_abnormally"
|
| reason | string The ORIGIN of the termination, not a diagnosis by elimination. |
{- "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"
}The PROVIDER layer of the capabilities (ADR 0041) for the nine providers, read from the same instances that serve the calls. A capability is only true when proven against the real API. The screen reads routingPreference from here before offering a routing preference on a binding (ADR 0166).
[- {
- "provider": "openrouter",
- "capabilities": {
- "streaming": true,
- "toolCalling": true,
- "listModels": true,
- "embeddings": false,
- "routingPreference": false,
- "stickySession": false,
- "priceCeiling": false,
- "promptCaching": false
}
}
]Agent → area → project → workspace cascade, WITHOUT a session: it is the agent's configuration on the project, not that of a specific conversation. origin: "agent" means this agent DIVERGED from its area's default; any other origin means it inherits.
| projectId required | string |
| agentSlug required | string Example: dev-api |
{- "modelId": "01JC4Z0000MODELO00000000001",
- "origin": "project",
- "routingPreference": null,
- "skipped": [
- {
- "scope": "agent",
- "modelId": "01JC4Z0000MODELO00000000001",
- "reason": "unavailable"
}
], - "herdadoDoLead": "secops"
}Applies to every session in this project that has no binding of its own, and only to those — before ADR 0064 the binding was global and reached other projects too. This is how a cheap model is given to the Psychologist and an expensive one to the Architect in the same project, and also how an agent DIVERGES from its area's default.
| projectId required | string |
| agentSlug required | string Example: dev-api |
| modelId required | string <uuid> Model to pin to this scope. Must exist in |
| routingPreference | string or null Enum: "price" "throughput" "latency" How a HUB picks the upstream that serves the model (ADR 0166, RN-583). ABSENT keeps the stored value when the new model's provider accepts it, and clears it otherwise; |
{- "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"
}DELETES the agent's binding — it does not copy the area's model into it. Copying would look the same on screen and is not: it would become a copy, and the area's next change would silently leave this agent behind. 404 when it already inherits.
| projectId required | string |
| agentSlug required | string Example: dev-api |
A SLIDING 30-day window, not the current calendar month: that is the label the screen shows, and with a calendar month the estimate would plunge on the 1st from the page turning over, not from a change in usage. Only actor_kind = agent counts (RN-038). An agent that never ran does not appear in the list — the screen shows a dash for it, which is different from zero.
| projectId required | string |
[- {
- "actorId": "dev-api",
- "costMicros": 3400000,
- "inputTokens": 128000,
- "outputTokens": 14200
}
]The default the lead and the area's subagents share. origin: "area" means someone chose it for this area; any other origin means the area itself inherits from the project or the workspace.
| projectId required | string |
| areaKey required | string Example: qa |
{- "modelId": "01JC4Z0000MODELO00000000001",
- "origin": "project",
- "routingPreference": null,
- "skipped": [
- {
- "scope": "agent",
- "modelId": "01JC4Z0000MODELO00000000001",
- "reason": "unavailable"
}
], - "herdadoDoLead": "secops"
}Applies to the lead and to every subagent of the area that has no binding of its own (RN-102). Requires maintainer because it reaches the whole area at once, and choosing a model is deciding spend — the same reason as the parallelism cap.
| projectId required | string |
| areaKey required | string Example: qa |
| modelId required | string <uuid> Model to pin to this scope. Must exist in |
| routingPreference | string or null Enum: "price" "throughput" "latency" How a HUB picks the upstream that serves the model (ADR 0166, RN-583). ABSENT keeps the stored value when the new model's provider accepts it, and clears it otherwise; |
{- "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"
}The area stops having its own default and starts inheriting from the project (or the workspace). Agents that diverged keep diverging: their binding is a different one, and deleting this one cannot decide for them.
| projectId required | string |
| areaKey required | string Example: qa |
Values in micro-USD. Requires maintainer — it is cost information.
| 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"
}The limit comes in as DOLLARS and is converted to micro-USD. With policy=block the call that would exceed the cap is REFUSED, not just recorded.
| projectId required | string |
| limitUsd required | number >= 0 Cap in DOLLARS — the only place in the contract that speaks in dollars, because that is what a person types. The api converts it to micro-USD, and responses come back in micro-USD. |
| policy required | string Enum: "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"
}This scope's RAW binding — null if the project only inherits from the 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> Model to pin to this scope. Must exist in |
| routingPreference | string or null Enum: "price" "throughput" "latency" How a HUB picks the upstream that serves the model (ADR 0166, RN-583). ABSENT keeps the stored value when the new model's provider accepts it, and clears it otherwise; |
{- "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"
}Batch form of GET .../agent-bindings/:agentSlug and GET .../area-bindings/:areaKey: each requested key gets EXACTLY the value its single-key route answers (same cascade, same origins, null when no level has a model). It exists so the Settings tab reads 20 resolved bindings with one request instead of 20 — the rate limit is per USER. Keys are comma-separated, deduplicated, and a malformed key or more than 64 keys in total is 400, never silently dropped.
| projectId required | string |
| agents | string Example: agents=criativo,po,dev-lead Comma-separated agent slugs. |
| areas | string Example: areas=dev,qa,infra Comma-separated area keys. |
{- "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"
}
}
]
}The local category are Ollama's, which spend no money; cloud are the paid API ones. A model the workspace has not activated does not appear here, but stays in the historical costs — to see it use the catalog route. The list is the project's OWNER workspace: the same model can be on in one workspace and off in the neighboring one (ADR 0049).
| projectId required | string |
{- "local": {
- "ollama": [ ]
}, - "cloud": {
- "anthropic": [ ],
- "openai": [ ]
}
}This is what feeds the session screen's live token meter.
| 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"
}Same dollar-to-micro-USD conversion as the project budget.
| sessionId required | string |
| limitUsd required | number >= 0 Cap in DOLLARS — the only place in the contract that speaks in dollars, because that is what a person types. The api converts it to micro-USD, and responses come back in micro-USD. |
| policy required | string Enum: "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. delta frames carry the incremental text and done closes with the token and cost accounting. A metering_failed frame means the RESPONSE went out but the cost was not accounted for — the failure shows up instead of disappearing. If the budget is exceeded with policy=block, the stream carries error and no delta. This route carries NO agent: it sends the text alone to the bound model, with no history and no system prompt. In a consultiva session where no agent was ever activated it is refused before any effect (RN-682) — talk to an agent through .../agents/:agent/message instead.
| projectId required | string |
| sessionId required | string |
| text required | string The user's message. The response comes back as an SSE stream. |
{- "text": "Summarize what we have already decided about checkout."
}Returns the binding RESOLVED by the cascade, not the session's raw binding: a session with no binding of its own uses the project's, and answering null here would be the wrong answer to the right question. The origin field says which scope the value came from.
agentId is optional and is the agent REALLY active in the session right now (the same one RunLlmTurnUseCase uses to run the turn) — without it, the cascade only sees session→project→workspace (plus the fixed fallback to Creative) and never reflects PO/Architect/Dev Lead/area after a handoff.
| projectId required | string |
| sessionId required | string |
| agentId | string Agent currently active in this session (e.g. "po", "arquiteto", "dev-lead"). |
{- "modelId": "01JC4Z0000MODELO00000000001",
- "origin": "project",
- "routingPreference": null,
- "skipped": [
- {
- "scope": "agent",
- "modelId": "01JC4Z0000MODELO00000000001",
- "reason": "unavailable"
}
], - "herdadoDoLead": "secops"
}Beats everything else in the cascade for as long as the session lives.
| projectId required | string |
| sessionId required | string |
| modelId required | string <uuid> Model to pin to this scope. Must exist in |
| routingPreference | string or null Enum: "price" "throughput" "latency" How a HUB picks the upstream that serves the model (ADR 0166, RN-583). ABSENT keeps the stored value when the new model's provider accepts it, and clears it otherwise; |
{- "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"
}The data was always in token_usage, but without aggregation or a route the team panel had no way to show the cost per card. Same role required as the session budget.
| sessionId required | string |
[- {
- "actorId": "dev-api",
- "costMicros": 3400000,
- "inputTokens": 128000,
- "outputTokens": 14200,
- "lastCall": {
- "inputTokens": 42000,
- "modelName": "qwen2.5-coder:7b",
- "contextWindow": 128000
}
}
]The MEMBER's audience. Only the caller's own rows — the actor comes from the authenticated token and there is no parameter to swap it. In tokens and ESTIMATED cost, without breaking down by provider or by credential: the key that ran is the owner's (RN-058) and its invoice is theirs (RN-060). The provider axis came to exist in the owner's report (ADR 0076) and remains unreachable from here — there is no dimension parameter on this route, and the repository type refuses the combination (RN-187). Agent doesn't count: token_usage records who spent, not who ordered the spend.
| projectId required | string |
| dias | string Sliding window. Default 30, max 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 Sliding window. Default 30, max 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
}
]
}Exists because the key agents spend is the workspace OWNER's (RN-058) — whoever pays the bill needs to see the bill. Groups by provider because that is the unit of the credential, and separates what went out by AGENT from what went out by a person in chat: the two come out of the same key and answer different questions. Returns no secret at all.
| 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
}
]
}
]
}Filters to OFFICIAL publishers only by default (the allowlist in domain/llm/huggingface-official-publishers.ts) — same "manual curation always" spirit as the LLM catalog (ADR 0042), applied to the "official" badge instead of to activation. includeCommunity=true brings in every publisher, each one tagged official: false for the screen to render the security warning — never hidden.
| q required | string Search text. |
| includeCommunity | string Pass |
[- {
- "repoId": "meta-llama/Llama-3.1-8B-Instruct-GGUF",
- "publisher": "meta-llama",
- "downloads": 182034,
- "likes": 210,
- "official": true
}
]First of two explicit steps: this call only records the intent, in pending_confirmation. Nothing is pulled until a separate call to POST .../pull-requests/:id/confirm — the product never runs an automatic, silent pull.
| workspaceId required | string |
| repoId required | string
|
| estimatedSizeBytes | number Comes from the search when the Hub published the size — never guessed by the client. |
{- "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"
}Moves pending_confirmation → confirmed → pulling, calls the Ollama daemon, and on success upserts the model into the catalog (models) and activates it for THIS workspace. On failure the request ends in failed with a reason declaring its origin (infra | model | code | policy) — never a silent failure. This call runs SYNCHRONOUSLY for the whole pull (see the use case for why); use the GET route below to poll instead of holding this connection open.
| 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"
]
}This scope's RAW binding. It is the root of the cascade.
| 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"
}Applies to every project that has no binding of its own.
| workspaceId required | string |
| modelId required | string <uuid> Model to pin to this scope. Must exist in |
| routingPreference | string or null Enum: "price" "throughput" "latency" How a HUB picks the upstream that serves the model (ADR 0166, RN-583). ABSENT keeps the stored value when the new model's provider accepts it, and clears it otherwise; |
{- "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 Enum: "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"
}Most recent first. It is an immutable log: never gets an UPDATE, and the row carries the before/after pair so the audit does not depend on reconstructing the "before" from the previous row.
| 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"
}
]Applies from now on and NEVER re-prices the past: each token_usage row keeps the price that produced its cost. The change stays recorded with the before/after pair — queryable on the history route.
| modelId required | string |
| inputPricePerMillionMicros required | number Price per million INPUT tokens, in micro-USD. |
| outputPricePerMillionMicros required | number Price per million OUTPUT tokens, in 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"
}Owner's curation, APPLYING ONLY TO THIS WORKSPACE (ADR 0049). Only touches isActive — availability is what the sync observed at the provider, is global, and does not change here. Whole batch or nothing: a nonexistent id fails the call without applying any row.
| workspaceId required | string |
| modelIds required | Array of strings <uuid> [ items <uuid > ] Models to turn on or off. Whole batch or nothing. |
| 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
}
]The curation screen. A model discovered by the sync comes in as isActive: false and only appears here until someone activates it; a model that disappeared from the provider comes with availability: "unavailable" and is never deleted, because bindings and cost history point to it. isActive is FOR THIS workspace (ADR 0049); availability is global, because it is what the sync observed at the provider.
| workspaceId required | string |
{- "local": {
- "ollama": [ ]
}, - "cloud": {
- "anthropic": [ ],
- "openai": [ ]
}
}Same use case as the internal route the engine schedules — the curation screen's refresh button has no path of its own, so there are never two reconciliations that could diverge.
{- "porProvider": [
- {
- "provider": "openai",
- "descobertos": 3,
- "reencontrados": 1,
- "indisponibilizados": 0,
- "pulado": "sem_capability",
- "origemDaFalha": "infra",
- "detalhe": "openai responded with status 401"
}
]
}The axis no catalog publishes: "good for code" is not a capability declared by any provider, it is the operator's opinion — and applies ONLY to this workspace (ADR 0049). Axis independent of activation: marking a use does not turn the model on in the selector, and changing the use does not turn off what was on. The uses list REPLACES the previous one.
| workspaceId required | string |
| modelIds required | Array of strings <uuid> [ items <uuid > ] Models to mark. Whole batch or nothing. |
| uses required | Array of strings Items Enum: "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
}
]The OWNER's audience. The PROVIDER axis came back (ADR 0076, RN-186) and is here, not on the member's route, because breaking down by provider is breaking down by CREDENTIAL — and credential is whoever pays's business (RN-060). Complements credential-spend, which keeps answering the INVOICE question (per month, with the tie to the key that exists today). Person and agent also come in separate blocks, by actor_kind. Sliding window in days, and the daily series comes DENSE — a day with no spend comes in as zero, otherwise the sparkline lies about the pace.
| workspaceId required | string |
| dias | string Sliding window. Default 30, max 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 Sliding window. Default 30, max 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. Unique within the workspace. |
| maxConsecutiveBlocked | number Circuit breaker (Phase 12b — RN-047): how many consecutive tasks ending blocked stop the module's dev agent at idle_tripped. Omitted uses the domain default (DEFAULT_MAX_CONSECUTIVE_BLOCKED). Applies from the NEXT execution activation onward — doesn't affect agents already running. |
| storyPromotion | string Enum: "manual" "auto" Who promotes a story from |
| language | string <= 35 characters The PROJECT's language (RN-619) — shared artifacts and turns with no human author use it. Any BCP-47 code the server recognizes, stored canonical; an unrecognized one is a 400. Omitted, the project starts with its creator's effective response language (RN-618: account choice > confirmed detection > interface language). |
{- "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"
}Takes its sessions, event log, and proposed actions along with it.
| 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"
}While the status is sem_decisao, the project container does not come up and the Code tab responds 409 — that's the RN-105 gate. Seeing the state requires the same permission as seeing the project, so 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 when the project was never provisioned — the common case today, because no real orchestrator transitions this table yet (RN-243/RN-267). status/imageVersion/resources are what was RECORDED; observado is what the Docker daemon reports right now, asked through the broker (ADR 0130). The two are never fused: a container killed from the outside reads as recorded running and observed exited. When there was no way to look, observado is null and naoObservado says why — it never inherits the recorded state (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 |
Array of objects (PortaDto) Up to 10 TCP ports published on 127.0.0.1 only. | |
| dns | Array of strings Up to 3 literal IPs. |
Array of 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
}Migrates execution_mode/workspacePath for a project that ALREADY exists (RN-447..450, ADR 0111) — dedicated route, separate from PATCH, because it orchestrates more than a column write: it moves permissions.json to the new scope root, retires the container lifecycle row when leaving container mode, and refuses (409) while any dev agent of the project is non-idle, since a running agent doesn't re-resolve its worktree location on its own.
| projectId required | string |
| executionMode required | string Enum: "container" "mounted" "runner" The new execution mode. It may repeat the current project mode — in that case, only |
| workspacePath | string The absolute path of the user folder — required when |
{- "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"
}Only who was associated HERE, with the role of that association — whoever reaches the project through the workspace alone has no row and doesn't show up.
| projectId required | string |
[- {
- "userId": "01JC4Z0000USUARIO0000000002",
- "role": "maintainer",
- "name": "Senior Dev",
- "email": "dev@brabo.dev"
}
]This role OVERRIDES whatever the person has in the workspace, in both directions: associating someone as viewer here really does restrict a workspace developer on this project. Two movements are refused with 403 and cannot be enabled anywhere: downgrading a workspace owner, and changing your OWN role — down or up, since self-promotion is the half that escalates privilege.
| projectId required | string |
| userId required | string <uuid> Id of the user to associate. |
| role required | string Enum: "viewer" "developer" "maintainer" "owner" Role in this association. On a PROJECT it OVERRIDES the workspace role in both directions — the effective role is this one whenever the association exists, higher OR lower — and two movements are refused with 403: downgrading a workspace |
{- "userId": "3f1b2c8e-5a4d-4b7e-9c10-2d6f8a1b4c33",
- "role": "developer"
}{- "projectId": "01JC4Z0000PROJETO0000000001",
- "userId": "01JC4Z0000USUARIO0000000002",
- "role": "maintainer",
- "createdAt": "2026-07-22T08:05:00.000Z"
}Removes only the PROJECT association. Whoever has a role in the workspace keeps seeing the project through inheritance — and because the project role OVERRIDES the workspace one in both directions, that inheritance can be LOWER than what the row granted. Removing your own row is therefore refused with 403 whenever the net effect is a downgrade (including when there is no workspace role at all); removing it when the workspace holds the same role, and removing anyone else, still work.
| projectId required | string |
| userId required | string |
The absolute path, ON THE USER MACHINE, where the local agent copies the work to (RN-515, ADR 0147) — a folder OUTSIDE the mounted base, which is the whole reason the mirror exists. Per project and never global: one global destination would land project B artifacts in project A's folder, and the user would find out from the contents, not from an error. mirrorPath: null CLEARS it and turns the mirror off — the key is required, omitting it is a 400, because a body that omits the field would be indistinguishable from asking to clear. null is the NORMAL state of a project. maintainer, the same minimum as execution-mode and projects-base: this route talks about a path on the operator's own filesystem. It validates ONLY the LEXICAL shape and refuses, with 400, a destination inside workspacePath or containing it (both directions of the same loop), and any destination at all on a container project. It never touches disk: the API cannot see the machine where the destination will live. Writing the mirror is NOT a proposed_action — it is configuration the user declared, not an agent asking to act.
| projectId required | string |
| mirrorPath required | string or null The absolute path, ON THE USER'S MACHINE, where the local agent copies the project work to (RN-515, ADR 0147). Send |
{- "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"
}Telemetry the local agent pushed over the channel after copying the work to the user folder (ADR 0147, point 7) — never the event log, because a mirror round has no session and session_events.session_id is NOT NULL (the same reasoning that made rag_searches a table). THREE answers that never collapse into one (RN-088): never (no round ever reported), synced (the last round copied — filesCopied may be 0, which means "looked and there was nothing to copy") and failed. A failure never erases the last successful sync, and a success never erases the last error: which one is CURRENT comes from comparing the two timestamps, so the screen can say "failing since today, last good copy was yesterday with 412 files". lastDestination is FROZEN — it diverges from mirrorPath after someone changes the destination, because the grant travels in the join and only changes when the runner reconnects (RN-516). viewer, the same minimum as reading the project, which already carries 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
}It's the physical file at the root of the project's workspace, not a database column.
| projectId required | string |
{- "allow": [
- "Terminal(pnpm test:*)",
- "Terminal(pnpm lint)"
], - "deny": [
- "Terminal(rm -rf *)",
- "Terminal(git push --force*)"
], - "ask": [
- "Terminal(pnpm add *)"
]
}Replaces all three lists at once. deny still beats allow in evaluation — there's no way to allow something denied through here.
| projectId required | string |
| allow required | Array of strings Automatically approved patterns. |
| deny required | Array of strings Forbidden patterns. |
| ask required | Array of strings Patterns that always ask for a human decision. |
{- "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 *)"
]
}Never includes the raw token — only what was already issued.
| projectId required | string |
[- {
- "id": "01JC4Z0000PAT000000000001",
- "name": "laptop",
- "projectId": "01JC4Z0000PROJETO000000001",
- "createdAt": "2026-08-22T12:00:00.000Z",
- "expiresAt": null,
- "revokedAt": null,
- "lastUsedAt": null
}
]The raw token (brb_...) appears only in THIS response — it is not recoverable later, only the hash is stored. Use it in --token/BRABO_ACCOUNT_TOKEN of brabo-runner.
| projectId required | string |
| name required | string A name for you to recognize this token later — not unique. |
| expiresInDays | number Expires in N days. Omitted = no expiration (revocable at any time, but never expires on its own). |
{- "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..."
}Idempotent — revoking again is not an error. Since RN-685 (ADR 0201) it also DROPS the runner connections opened with THIS token, and only those — the same user's device key stays up —, and voids its still-unused tickets. The engine being down does NOT make the revocation fail.
| projectId required | string |
| tokenId required | string |
Incident response — a departed dev with a leaking token (RN-427). Idempotent — revoking again is not an error. Since RN-685 (ADR 0201) it also DROPS the runner already connected with this token, and only it.
| projectId required | string |
| tokenId required | string |
The maintainer view for incident response (RN-427) — includes the owner of each token. Never includes the raw token.
| 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"
}
]Nobody revokes what they cannot see (RN-519). Includes the already REVOKED ones — dropping the row would make the screen claim the key never existed. Never returns the public JWK, and the api never saw the private one. A null lastUsedAt is the sign of an ORPHAN key: registered and never used by any runner. Since RN-543 it also includes the caller's MACHINE keys (especie: "maquina", null projectId), which serve this project without belonging to it — without them in the list, a machine key would be invisible on every screen.
| projectId required | string |
[- {
- "id": "01JC4Z0000CHAVE000000000001",
- "name": "laptop",
- "projectId": "01JC4Z0000PROJETO000000001",
- "especie": "projeto",
- "createdAt": "2026-08-27T12:00:00.000Z",
- "revokedAt": null,
- "lastUsedAt": null
}
]Idempotent — revoking again is not an error. Since RN-520 it also DROPS the live connection: before, revoking only prevented a NEW ticket, and an already connected runner kept running approved commands. Since RN-685 (ADR 0201) the target is the KEY: only the connections opened with it drop — in every project, if it is a machine key —, and another runner of the same user, connected with a PAT or another key, stays up. Its still-unused tickets are voided. The engine being down or no runner connected does NOT make the revocation fail.
| projectId required | string |
| deviceKeyId required | string |
Authenticates connect/3 of the Phoenix socket /runner (topic terminal:<projectId>, role "runner") — it is NOT the reused JWT, and this route itself only accepts a Personal Access Token instead of the session JWT (ADR 0105). 30s TTL and single use. Refuses with 400 if the project is not in "runner" mode (RN-421): the runner has nothing to serve in a project whose code lives in the managed container.
| projectId required | string |
{- "ticket": "string",
- "expiresAt": "2026-08-09T12:00:30.000Z",
- "engineWsUrl": "ws://localhost:4000/runner"
}Same /runner socket, same topic, role "terminal" — whoever SEES the terminal (the command itself only really runs if a runner is connected). Valid for any project mode.
| projectId required | string |
{- "ticket": "string",
- "expiresAt": "2026-08-09T12:00:30.000Z",
- "engineWsUrl": "ws://localhost:4000/runner"
}Authenticated by a MACHINE device key (ADR 0154) — NEVER by a session JWT, and never by a credential bound to a project, which answers 403. Returns the projects in execution_mode: "runner" in which the key owner reaches at least developer (the same minimum as POST .../runner-ticket), with the folder name and verification state of each. The agent asks instead of scanning the disk: the machine base belongs to the user and may hold folders that are not projects.
[- {
- "projectId": "01JC4Z0000PROJETO000000001",
- "name": "Brabo",
- "workspaceDirName": "brabo-01jc4z",
- "workspaceVerifiedAt": null
}
]One row per project of the workspace — since RN-521 a project that never provisioned a container is PRESENT with registrado: null instead of absent, because this page is the human path to start the first one. The observed state is asked of the broker only for rows provisioning/running, and only up to a per-load budget — see naoVerificado on rows that were skipped, and ADR 0136 for the reasoning.
| 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 is pending (the default) or failed. actionType filters by type — e.g. git_merge, for the PRs tab to find the merge proposal for a PR without knowing which session created it. status=failed REQUIRES actionType and lists the actions whose execution failed — the PRs tab reads the last refused merge of each PR from it (a merge conflict carries executionResult.conflictingFiles).
| projectId required | string |
| status | string Example: status=pending |
| actionType | string Example: 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"
}
]Only the explicitly recorded rules. An agent with no row here falls back to the project's permissions.json default.
| projectId required | string |
[- {
- "agentId": "dev-api",
- "actionType": "terminal",
- "mode": "auto_approve"
}
]Upsert by (agent, type). It does NOT override permissions.json: a pattern already in deny stays blocked no matter how much autonomy is granted here. actionType: "*" is "auto mode" (RN-153) — autonomy for ANY action type of this agent; a specific rule recorded afterwards still wins over the wildcard for that type.
| projectId required | string |
| agentId required | string Agent slug. |
| actionType required | string Enum: "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" "*" Action type, or |
| mode required | string Enum: "auto_approve" "require_approval" "deny" This agent's autonomy for this type. Does NOT override |
{- "agentId": "dev-api",
- "actionType": "terminal",
- "mode": "auto_approve"
}Ordered by seq; use nextCursor as afterSeq. latest=true fetches the TAIL instead (the newest limit actions, still ascending) and ignores afterSeq. status=pending keeps only the actions waiting for a decision — with latest, the newest pending ones, so a session with more than 200 actions does not push a new pending one out of the window (RN-637).
| projectId required | string |
| sessionId required | string |
| afterSeq | string Example: afterSeq=6 |
| limit | string Example: limit=50 |
| latest | string Example: latest=true Fetches the tail of the session; ignores |
| status | string Example: status=pending Only |
{- "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
}The action is evaluated against the project's permissions.json on creation. The response's resolvedPolicy says what the policy decided: auto_approve comes out already executable, require_approval waits for a human decision, and deny is born rejected. There is no path that executes without going through here.
| projectId required | string |
| sessionId required | string |
| actionType required | string Enum: "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" Action type. Each type has its own minimum role: |
required | object Who is proposing it. |
required | object Action parameters, with a shape specific to the |
{- "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"
}Applies only to this action. The decision stays in the event log with the id of who decided.
| 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"
}Besides releasing this action, it adds the corresponding pattern to the project's allow list — future matching actions come out auto_approved without asking. A pattern already in deny stays blocked. The approval and the pattern are recorded in the same transaction. Clicking an action that was already APPROVED is idempotent success (desfecho: ja_aprovada), recording the pattern only if it is missing.
| 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
}The reason, if given, stays in the event log alongside the decision.
| projectId required | string |
| sessionId required | string |
| actionId required | string |
| reason | string Stays in the event log alongside the denial. Optional, but recommended. |
{- "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"
}The project is born with the three areas (RN-094), so the list comes back full even before there is a module_map. What activating execution adds are the MEMBERS of the dev area, one per module.
| projectId required | string |
[- {
- "id": "01JC4Z8QK3M7YV2N5T9B0PXHRA",
- "projectId": "01JC4Z8QK3M7YV2N5T9B0PXHRA",
- "key": "dev",
- "leadAgentId": "dev-lead",
- "maxParallel": 2,
- "budgetMicros": 20000000,
- "spentMicros": 4300000,
- "members": [
- "dev-api",
- "dev-web"
]
}
]Requires maintainer, same reason as the parallelism cap: this is an INDEPENDENT, additive check next to the project and session budgets (ADR 0110) — not a cascade, and not a replacement for either. The limit comes in as DOLLARS and is converted to micro-USD server-side, same convention as BudgetsController. null clears the cap.
| projectId required | string |
| key required | string |
| limitUsd required | object or null >= 0 Cap in DOLLARS for the area — same dollar-in, micro-USD-out convention as the project/session budgets ( |
{- "limitUsd": 20
}{- "id": "01JC4Z8QK3M7YV2N5T9B0PXHRA",
- "projectId": "01JC4Z8QK3M7YV2N5T9B0PXHRA",
- "key": "dev",
- "leadAgentId": "dev-lead",
- "maxParallel": 2,
- "budgetMicros": 20000000,
- "spentMicros": 4300000,
- "members": [
- "dev-api",
- "dev-web"
]
}Requires maintainer, not developer, for the same reason as activating execution: raising the cap is deciding how much the product can spend without asking. Applies to the NEXT requests — whatever is already awaiting your decision keeps waiting.
| projectId required | string |
| key required | string |
| maxParallel required | number >= 1 How many agents the area's lead can have in the session WITHOUT requesting your authorization. Minimum 1 — zero does not mean "unlimited", it means invalid configuration, and treating it as unlimited would turn a typo into unrestricted spending. |
{- "maxParallel": 4
}{- "id": "01JC4Z8QK3M7YV2N5T9B0PXHRA",
- "projectId": "01JC4Z8QK3M7YV2N5T9B0PXHRA",
- "key": "dev",
- "leadAgentId": "dev-lead",
- "maxParallel": 2,
- "budgetMicros": 20000000,
- "spentMicros": 4300000,
- "members": [
- "dev-api",
- "dev-web"
]
}Requires maintainer, not developer, because the dev agents' git actions inherit the role of WHOEVER ACTIVATED it in the IAM evaluation — activating as developer would leave agents unable to open a PR. Each module in the module_map gets an agent in an isolated worktree.
| projectId required | string |
| taskBudgetMicros | number Budget per task in micro-USD. Omitted uses the domain default (DEFAULT_TASK_BUDGET_MICROS). |
| maxGateCorrections | number How many dev↔gate round trips before the task becomes |
| devAgentImpl | string Enum: "real" "noop"
|
| terminalAllowPatterns | Array of strings Patterns released in |
| originSessionId | string Id of the CHAT session the "activate execution" click came from — the Dev Lead/PO's creative/consultative session, never the execution one. Omitted (legacy caller, e.g. activation from the Overview without session context) closes no session, same as before. When given, it is closed at the end — but only if it has no pending handoff/action/turn (RN-135), and never if it is the execution session itself. |
{- "taskBudgetMicros": 500000,
- "maxGateCorrections": 3,
- "devAgentImpl": "real",
- "terminalAllowPatterns": [
- "Terminal(pnpm test:*)",
- "Terminal(pnpm lint)"
], - "originSessionId": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
}{- "sessionId": "01JC4Z8QK3M7YV2N5T9B0PXHRA",
- "modules": [
- "api",
- "web"
]
}The most recent active session that already carries execution.activated — the SAME criterion activate uses to decide whether to reactivate or create one (RN-139). null when no execution is in progress. Exists so the Executors tab stops inferring this session from the project's most recent one, which shifts from session to session with no clue at all in the UI.
| 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"
}Resets the consecutive blocked-tasks counter (RN-047) and puts the agent back to trying to claim work. It is the only way out of idle_tripped — there is no automatic unblock.
| projectId required | string |
| sessionId required | string |
| agentId required | string |
{- "ok": true
}Goes through the dev area CAP (RN-083): under it, the agent starts right away, in its own worktree; above it, the response is a proposed_action of type parallelize awaiting your decision, and NOTHING starts until you decide. What the cap is set to is configurable per lead in Settings.
| projectId required | string |
| sessionId required | string |
| module required | string Module name, as it appears in the |
{- "module": "api"
}{- "estado": "aguardando_autorizacao",
- "actionId": "01JC4Z8QK3M7YV2N5T9B0PXHRA",
- "ativosNaSessao": 2,
- "maxParallel": 2,
- "motivo": "string"
}Resets the gate correction counter and hands the task back to the agent. It is the human escape hatch for the correction cap — there is no automatic unblock.
| projectId required | string |
| sessionId required | string |
| taskId required | string |
{- "ok": true
}Most recent first, each already carrying its diff against the previous one, computed server-side.
| projectId required | string |
| agent required | string Example: 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
}
}
]The history is immutable: restoring deletes nothing, it CREATES a new version with the old content. Requires maintainer because it changes the agent's behavior from that point on — same calibration as the patch.
| projectId required | string |
| agent required | string Example: dev-api |
| version required | string Example: 2 Version to restore. |
{- "agent": "dev-api",
- "restoredFrom": 2,
- "toVersion": 5,
- "cacheInvalidated": true
}Different from the event-by-session route: here the session isn't known. This is what makes the profile's evidence chip land on the right event, since the Anamnese's window spans several sessions.
| 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"
}Starts from whoever HAS a version in the project, not from a static roster — this is what makes the per-module dev agents (dev-api) show up.
| 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
}
}
]
}
]The user's OWN profile by default; the aggregated team view only for whoever administers the project. A competency profile is data ABOUT the person, and the least surprising default is seeing your own.
| 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"
}
]Only the caller's OWN profile, never someone else's. The opt-out comes along because without it the next round would re-derive everything and the deletion would be cosmetic. It is viewer on purpose: profiling covers all members, so requiring developer would leave a profiled viewer unable to delete their own.
| projectId required | string |
{- "deleted": 2,
- "optedOut": true
}The current map is the one with the highest version (history is immutable). The pending items are the story↔map cross-validation: a story with no module, or pointing at a module that doesn't exist.
| 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": { }
}
}Nested and complete, in a single call. ARCHIVED stories (RN-727) are not listed. Backlog is written by agents, through the /internal/* routes.
| 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"
}
]
}
]
}
]Cross-references the project's rules with the stories' businessRuleIds. A rule with no story at all is a DISCOVERY — a pending item for the PO, not an error.
| projectId required | string |
{- "rules": [
- {
- "ruleId": "RN-014",
- "title": "Cart accepts at most 50 items",
- "coveredByStoryIds": [
- "01JC4Z0000HISTORIA000000001"
], - "covered": true
}
], - "uncoveredCount": 2
}InfraAgent's artifacts (Dockerfile, compose, CI). They go through the SAME QA and SecOps gates as dev PRs, with no task or worktree behind them.
| 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"
}
]Status comes from the event log: open is an artifact.open_question without open_question.answered. Newest first, capped at 50 with total.
| projectId required | string |
| status | string Default: "open" Enum: "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
}Only the author of the session where the question was born answers. Writes a decision_record (actor: the user, with questionId) and open_question.answered, and asks the engine to resume the parked QA gate of each affected task (RN-860), best effort.
| projectId required | string |
| questionId required | string |
| resposta | string Free-text answer. Send this OR |
| aceitarSugestao | boolean Accept the agent's suggestion as the answer. |
{- "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): the current artifact.run_instructions, emitted when an open_infra_pr with run executes. DECLARED by the Infra agent, never verified on the reader's machine. null when there is none.
| 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"
}The story leaves the backlog, the coverage, the promotion queue, the Dev Lead's plan and the claim — and so do its tasks. Nothing is deleted: the row stays with archivedAt, and backlog.story_archived records who, why and which tasks. Only a draft story with no task in execution.
| projectId required | string |
| storyId required | string |
| reason | string Optional; recorded in |
{- "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"
}The story leaves the proposal queue and the reason becomes a message pinned to the PO's session — the same pattern as returning a gate to dev. If that session's PO is no longer up, the return is recorded anyway: it's the user's decision and does not depend on an agent listening.
| projectId required | string |
| storyId required | string |
| reason required | string The reason is REQUIRED: it becomes a message pinned to the PO session, and a return with no reason hands back work with no information. |
{- "reason": "The acceptance criteria don't cover the payment refusal case."
}{- "ok": true
}Only a draft story with no task in execution (in_progress/in_review). Writes backlog.story_updated with the previous and the new title. Same role as the other backlog writes (promote/return).
| projectId required | string |
| storyId required | string |
| title required | string Trimmed; 1 to 200 characters. |
{- "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"
}The human step that manual mode (default since Phase 12c) hands back to the user: until promotion, NO task from the story is pickable by any dev agent. Promoting releases the batch of tasks at once and wakes up the idle agents of the module (Phase 12b). The batch is NOT all-or-nothing — whatever didn't pass comes back as failed with the reason, and whatever passed is promoted.
| projectId required | string |
| storyIds required | Array of strings Ids of the stories to promote to |
{- "storyIds": [
- "01JC2XK9Q7",
- "01JC2XKB4M"
]
}{- "promoted": [
- "string"
], - "failed": [
- {
- "storyId": "01JC2XK9Q7",
- "reason": "Story is not ready: missing dod, dor"
}
]
}Each hypothesis cites the events that support it (evidenceEventIds) — this is what makes it auditable instead of taken on faith.
| 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"
}
]Moves proposed → accepted. NOT a proposed_action: the Psychologist is read-only and nothing here has an external effect.
| 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"
}Moves proposed → dismissed. Same reasoning as accepting.
| 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 is summed from the round's token_usage. This is what makes the cost difference between light and heavy triage VISIBLE, instead of just asserted — the two tiers use genuinely different models.
| 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
}
]Read-only — has no side effect. enabled: false means the pause is a PRODUCT decision (not a bug): existing analyses and hypotheses aren't touched by it.
{- "enabled": false
}Requires maintainer, not developer, because it runs the ToolLoop again and SPENDS real budget — unlike accepting or dismissing, which only move local state. The previous analysis is marked superseded.
| projectId required | string |
| sessionId required | string |
{- "ok": true
}Real count (never estimated) of .md files under docs/docs/adr in the project's repository against how many have a chunk, and the project's sessions against how many have a chunk. Does not include "N minutes ago" for those three scopes — there is no per-scope indexing timestamp column, and a guessed number would lie (same rule as ADR 0042 for model rating). local (RN-455) is the one exception: lastAttachedAt is a real MAX(created_at) over that scope, because a browser upload has no repository state to compare against.
| 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
}The only signal of TRUTH the RAG measurement has: latency and degradation rate say whether the search RAN, never whether it was RIGHT. Same role as search — whoever can read the result is who can judge it. Re-voting the same chunk of the same search overwrites your own vote rather than adding a second one (unique per actor), so the metric measures accuracy and not enthusiasm.
| projectId required | string |
| searchId required | string <uuid> The |
| chunkId required | string <uuid> The chunk being judged. Must be one of the hits THAT search returned, otherwise 400. |
| verdict required | string Enum: "util" "irrelevante" Two values, not a 1-5 scale: a finer scale invites per-voter differences in ruler that no aggregation recovers later. |
{- "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
}Text content already read by the browser (<input webkitdirectory>), never a host path — that distinction is the whole point of ADR 0113. Idempotent full rebuild of the local scope: every call deletes and recreates it from the files sent THIS time, so re-uploading is the resync mechanism. Rejects (400) over the file-count/total-bytes caps instead of truncating silently — this is a one-shot user gesture with a folder picker in front of it, not a background scan. An individual oversized or unrecognized-extension file is only skipped, never fails the batch.
| projectId required | string |
| folderName required | string The folder name as chosen in the browser — shown in the coverage panel. |
required | Array of objects (LocalFolderFileDto) Up to 500 files. The exact summed-byte cap (RAG_LOCAL_TOTAL_BYTES_LIMIT) and the per-file extension allowlist are enforced by the use case, not by this DTO — an array of individually-valid strings can still add up to a payload too large. |
{- "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"
}
}"Reindex now" from the Chat RAG panel. Deletes and recreates the chunks of the three honest scopes (RN-219) from the CURRENT state — there is no automatic watcher on push/session close (decision recorded in ADR 0079/0080). Can take a while on projects with many sessions: it runs one indexing pass per session, each possibly calling the embedding provider in batch.
| 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"
}Combines cosine similarity (pgvector) with lexical ts_rank (tsvector), each from an independent query, merged by weighted sum and cut off by the threshold (ADR 0080). vectorAvailable: false warns when the embedding provider did not respond and the search ran with only the lexical signal — it never pretends to have run the full hybrid.
| projectId required | string |
| query required | string Between 2 and 500 characters — same rule as the Code tab search. |
| scopes | Array of strings Items Enum: "docs" "adr" "session" "local" When absent, searches all four scopes (RN-219/454). |
| limit | number 1 to 10 — when absent, uses the cap. |
{- "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"
}Each item carries technical: true only for the session opened by repository provisioning (linked from 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
}
]The session is born in created and is the container for everything agents do: the event log, proposed actions and the token budget all hang off it. kind is REQUIRED and gets recorded: consultiva (consultative) only converses, criativa (creative) produces and is the only one that accepts execution.activated.
| projectId required | string |
| kind required | string Enum: "consultiva" "criativa" The session's intent. |
| name | string <= 80 characters Friendly name, optional. Does not replace the id's hashtag — screens compose the two. Blank counts as absent. |
{- "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"
}The name is a navigation label and does NOT replace the id's hashtag — screens compose the two, and without a name they degrade to just the hashtag. null or a blank string clears the name. There is no way to change kind: it is the creation intent, and changing it later would turn it into state.
| projectId required | string |
| sessionId required | string |
| name required | object or null <= 80 characters The new name. |
{- "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
}Business rules, decision records and artifacts per agent counted over the WHOLE session log (the screen reads a 200-event tail), plus the files touched by git_commit/git_push in this session and in the execution session its approved Dev Lead plan activated (RN-828). Since RN-844 it also returns the ITEMS of rules and decisions (latest 50 each; the totals stay in regras/decisoes) and the files of the PRs opened in those sessions, read from the same diff the PRs tab reads, for at most 20 PRs per call; prsNaoLidas says how many were left out or failed. Read only; no new table.
| 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
}
}The log is IMMUTABLE and ordered by seq. Normal pagination is incremental: pass the previous response's nextCursor as afterSeq. latest=true does the opposite — it fetches the TAIL and ignores afterSeq, for a screen that opens at the end instead of reconstructing the whole session from the start (ADR 0021).
| projectId required | string |
| sessionId required | string |
| afterSeq | string Example: afterSeq=40 Returns the events with |
| limit | string Example: limit=50 |
| latest | string Example: latest=true Fetches the tail of the log; ignores |
| actionId | string Example: actionId=01JC4Z0000ACAO00000000000001 Only the events whose |
{- "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: seq is assigned by the server and nothing in the log is updated or removed afterward. There is no edit endpoint, by design.
| projectId required | string |
| sessionId required | string |
| type required | string Event type. The full inventory is in |
required | object (ActorDto) |
required | object Free-form shape, specific to each |
{- "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"
}Exists so the Psychologist's hypothesis evidence can reach the cited event regardless of which page it fell on or the feed's filters.
| 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"
}Only closed/closed_abnormally reopen, and only to active — closing never goes back (ADR 0183). The session keeps its event log, artifacts, answered questions and handoffs; kind is untouched. A NEW event session.reopened records the previous closedAt and terminationReason, which the row clears. A session that carries execution.activated is refused (sessao_com_execucao): open a new session and activate execution there. No time limit. Requires 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"
}Resolved for the CALLER, never for the session: session override > Account choice > detected-and-confirmed > interface language (RN-618). Returns the winner, its origin and every link of the chain — and, when your recent messages point to another language and the screen should ask about it, 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"
}Only for the caller, only in this session (RN-618) — the other participants and your Account choice are untouched. null goes back to inheriting from the Account.
| projectId required | string |
| sessionId required | string |
| language required | string or null <= 35 characters The language agents answer YOU in, in THIS session only (RN-618) — other participants are never affected. Any BCP-47 code the server recognizes, stored canonical. Send |
{- "language": "en"
}{- "language": "pt-BR",
- "origin": "interface",
- "sessionOverride": null,
- "account": "automatico",
- "detected": null,
- "interfaceLocale": "pt-BR",
- "detectionQuestion": "es"
}The ticket authenticates the session's Phoenix socket session:<id> connect/3 on the engine — it is NOT the reused JWT. 30s TTL and single use: every reconnection (including automatic ones) requests a new ticket. scope: "heartbeat" requires the viewer role; scope: "terminal" requires developer, the same minimum role as terminal actions.
| projectId required | string |
| sessionId required | string |
| scope required | string Enum: "heartbeat" "terminal"
|
{- "scope": "heartbeat"
}{- "ticket": "string",
- "expiresAt": "2026-08-09T12:00:30.000Z"
}The transition is validated by the domain's state machine, not by the client: created → active → closing → closed | closed_abnormally. An invalid jump is 409.
| projectId required | string |
| sessionId required | string |
| status required | string Enum: "created" "active" "closing" "closed" "closed_abnormally" Target state. The state machine refuses an invalid jump with 409 — from |
{- "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"
}The composer's "Stop" button (RN-122): kills the in-flight LLM call in the engine, cutting the connection mid-stream — it saves tokens for real, not just stops rendering on the client. Idempotent: with no turn in progress, it is accepted with no effect.
| projectId required | string |
| sessionId required | string |
| agent required | string Example: po Slug of the active agent. |
{- "ok": true
}The response is just the acknowledgment, and it returns on ACCEPTANCE — before the agent's turn ends (ADR 0163). What the agent replies arrives via the session's event log and channel — not through this call. If the agent is mid-turn, the message is NOT refused: it joins the agent's queue (entrega: "enfileirada") and is read, with any others queued, in one turn when the current one ends; it can be cancelled while it waits (RN-673, ADR 0191).
| projectId required | string |
| sessionId required | string |
| agent required | string Example: po Slug of the conversational agent that reads the message: criativo, po, arquiteto, dev-lead, ux-designer or staff. Any other slug — infra included — is refused with 422; it is never delivered to a default agent (RN-584). |
| text required | string [ 1 .. 10000 ] characters User's message to the session's active agent. |
{- "text": "I want to allow discount coupons at checkout."
}{- "ok": true,
- "mensagemId": "01JC4Z0000EVENTO000000000001",
- "entrega": "enfileirada",
- "posicao": 2
}A message sent while the agent is mid-turn waits in a queue and is read when the turn ends (RN-673). While it waits, the person who SENT it can cancel it: the engine removes it from the queue and records chat.message_cancelled; it is never shown to the model. Someone else's message is 403.
| projectId required | string |
| sessionId required | string |
| agent required | string Example: po Slug of the agent whose queue holds the message. |
| messageId required | string Example: 01JC4Z0000EVENTO000000000001 Id of the |
{- "ok": true
}Only the Criativo can be started by a direct command. Every other agent requires an ACCEPTED handoff pointing to it — the rule lives in the domain, not here, and trying to bypass it returns 409.
| projectId required | string |
| sessionId required | string |
| agent required | string Example: criativo Agent slug. |
{- "agent": "po",
- "status": "active"
}Records chat.structured_question_answered and resends the answers to the agent as a normal message. A question set can only be answered once. Returns on ACCEPTANCE, before the agent's turn ends (ADR 0163): the turn keeps running in the engine and its narration, end and failures arrive through the session channel and the event log (agent.status, agent.response, agent.error) — never through this response.
| projectId required | string |
| sessionId required | string |
| agent required | string Example: criativo Slug of the agent that asked the questions (and will read the answer). |
| questionSetId required | string Example: 01JC4Z0000EVENTO000000000001 Id of the answered |
required | object Answer keyed by question | ||
| |||
{- "answers": {
- "name": "Easy Checkout",
- "users": "Small business owners",
- "platform": "Web"
}
}{- "ok": true
}Dedicated endpoint instead of reusing readiness, which belongs to the Criativo: they are two different milestones of the session, and conflating them would make the event log ambiguous. Returns on ACCEPTANCE, before the agent's turn ends (ADR 0163): the turn keeps running in the engine and its narration, end and failures arrive through the session channel and the event log (agent.status, agent.response, agent.error) — never through this response. The Dev Lead handoff still comes AFTER the Infra one: the engine holds it until the closing turn ends. Idempotent (ADR 0182, RN-635): a target that already has a pending offer or is active in the project is not triggered again, and with both like that nothing is recorded.
| projectId required | string |
| sessionId required | string |
{- "ok": true,
- "desfecho": "confirmado",
- "jaAtendidos": [
- {
- "toAgent": "infra",
- "motivo": "oferta_pendente"
}
]
}Records necessity.validated. Requires the Criativo to have already consolidated a product_brief in this session (RN-406) — without it, it is refused: there is nothing to validate. Since ADR 0185 (RN-657) the "I'm ready — the need is validated" click (POST .../readiness) records this event itself, and the web no longer calls this route; it stays for sessions whose readiness click predates that 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"
}
]Born as offered, exactly like an agent's own offer_handoff — the only difference is who decided. toAgent has to be in the addressable catalog (area lead or area-less agent); a subagent or an unknown slug is refused with 400. At most one pending offer per (project, target): see desfecho (ADR 0182, RN-635).
| projectId required | string |
| sessionId required | string |
| toAgent required | string Slug of the target agent. Must be a lead or an area-less agent — never a subagent. |
{- "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"
}One thing only: transition to accepted AND start the destination agent. It is the normal activation path for every agent other than the 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"
}It's the button that triggers the product_brief and the handoff to the PO. Records readiness.confirmed in the event log. Returns on ACCEPTANCE, before the agent's turn ends (ADR 0163): the turn keeps running in the engine and its narration, end and failures arrive through the session channel and the event log (agent.status, agent.response, agent.error) — never through this response.
| 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 Enum: "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
}No role required: a credential is about whoever called, not about a workspace or project. Covers LLM keys and git tokens in the same listing.
[- {
- "id": "01JC4Z0000CREDENCIAL00000001",
- "provider": "anthropic",
- "createdAt": "2026-07-10T10:00:00.000Z",
- "updatedAt": "2026-07-10T10:00:00.000Z"
}
]Upsert by (user, provider). The key is encrypted via envelope encryption before touching the database, and the response carries only the PROJECTION — never the secret, not even encrypted.
| provider required | string Enum: "anthropic" "openai" "openrouter" "nvidia-nim" "together" "deepinfra" "bitdeer" "vultr" "9router" "opencode-zen" |
| apiKey required | string <= 512 characters The key in plain text. It is encrypted via envelope encryption before touching the database and NEVER comes back in any response — not even encrypted. The 512-character cap is protection against an absurd payload, not format validation: whether the key is any good is answered by the provider, at |
{- "provider": "anthropic",
- "apiKey": "sk-ant-api03-…"
}{- "id": "01JC4Z0000CREDENCIAL00000001",
- "provider": "anthropic",
- "createdAt": "2026-07-10T10:00:00.000Z",
- "updatedAt": "2026-07-10T10:00:00.000Z"
}The check is an explicit action, separate from registration (ADR 0050): the api decrypts the secret, calls the provider, and returns ONLY the verdict — the key never comes back, not even in part. Responds 200 for all three outcomes, because the request was processed; refused is an outcome, not a protocol error. 404 when there is no credential for (user, provider) — then there is nothing to test.
| provider required | string Example: openrouter |
{- "resultado": "recusado",
- "motivo": "connection test failed for openrouter: openrouter responded 401"
}Its own path, not the users/me/credentials one, because the body is different (token, and the provider enum is github/gitlab). The token is encrypted and stored without prior verification (ADR 0050); to verify it, POST /users/me/credentials/{provider}/test. Reading, removal, and testing reuse the users/me/credentials routes — it's the same table.
| provider required | string Enum: "github" "gitlab" |
| token required | string <= 512 characters Personal access token. Encrypted by envelope encryption before touching the database and never returned, not even encrypted. Registration does NOT test the token (ADR 0050) — to verify it use |
{- "provider": "github",
- "token": "ghp_…"
}{- "id": "01JC4Z0000CREDENCIAL00000001",
- "provider": "anthropic",
- "createdAt": "2026-07-10T10:00:00.000Z",
- "updatedAt": "2026-07-10T10:00:00.000Z"
}Account-level, with no project in the path (RN-611): the one-line install creates a machine key before any project exists, and every other listing is per project. Returns ONLY machine keys (especie: "maquina", projectId: null) and ONLY the caller’s — project keys stay in their project’s listing, and no one sees another user’s keys (RN-519). Revoked keys are INCLUDED: registering a new machine key revokes the previous one (RN-552), and this list is where that shows. lastUsedAt is a recorded use, never a live connection; null means the key was never used.
[- {
- "id": "01JC4Z0000CHAVE000000000001",
- "name": "laptop",
- "projectId": "01JC4Z0000PROJETO000000001",
- "especie": "projeto",
- "createdAt": "2026-08-27T12:00:00.000Z",
- "revokedAt": null,
- "lastUsedAt": null
}
]Idempotent — revoking again is not an error. Same revocation as DELETE /projects/{projectId}/runner-device-keys/{deviceKeyId}: it also drops the LIVE connections opened with this key, in EVERY project (RN-520/RN-543). Since RN-685 (ADR 0201) the target is the KEY: another runner of the same user, connected with a PAT or with another key, stays up. The key’s still-unused tickets are voided. With no project yet there is no connection to drop. A PROJECT key, a key that does not exist and another user’s key all answer the same 404.
| deviceKeyId required | string |
{- "locale": "pt-BR",
- "responseLanguage": "automatico",
- "detectedLanguage": null,
- "detectedLanguageConfirmedAt": "2019-08-24T14:15:22Z",
- "effectiveResponseLanguage": {
- "language": "pt-BR",
- "origin": "interface"
}
}Both fields are optional and independent — sending one never touches the other, and a body with neither is a 400. locale (the interface) stays closed to the pt-BR/en list. responseLanguage (RN-618) is automatico or any recognized BCP-47 code, stored canonical; an unrecognized code is a 400 and nothing in the body is written.
| locale | string Enum: "pt-BR" "en" "ru" "zh-Hans" The interface language. Closed to the list — not free-form BCP-47. |
| responseLanguage | string <= 35 characters The language agents answer in (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"
}
}The only way a detected language becomes a preference (RN-624): detection alone never changes anything. confirm writes detectedLanguage + detectedLanguageConfirmedAt; decline records the refusal so the same language is not asked again. Returns the preferences after the answer.
| language required | string <= 35 characters The language the question was about — the |
| answer required | string Enum: "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"
}
}Already filtered by association — there is no global listing. Each item carries the caller's own role.
[- {
- "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"
}
]Requires no role: whoever creates it becomes owner. It's the only entry point for someone who doesn't belong to any workspace yet.
| name required | string >= 2 characters |
| slug required | string^[a-z0-9]+(-[a-z0-9]+)*$ kebab-case. Unique in the system and visible in the 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"
}{- "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. Unique in the system and visible in the 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"
}Requires owner. Takes its projects, sessions, and their history along with it.
| 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"
}Who is associated with the WORKSPACE, with the workspace role — the role every project inherits unless a project row overrides it. Completes GET projects/:projectId/members, which lists only project rows: whoever reaches a project through the workspace alone shows up here and not there. Only id, name, e-mail and role.
| workspaceId required | string |
[- {
- "userId": "01JC4Z0000USUARIO0000000002",
- "role": "maintainer",
- "name": "Senior Dev",
- "email": "dev@brabo.dev"
}
]Only owner can touch the member roster. The role here is inherited by ALL of the workspace's projects. That role is NECESSARY but not SUFFICIENT: changing YOUR OWN role here is refused with 403 in both directions, and cannot be enabled anywhere. There is no level above to catch the fall, so a self downgrade would be unrecoverable through the UI. Demoting ANOTHER owner is still allowed — it is the only way ownership is revoked.
| workspaceId required | string |
| userId required | string <uuid> Id of the user to associate. |
| role required | string Enum: "viewer" "developer" "maintainer" "owner" Role in this association. On a PROJECT it OVERRIDES the workspace role in both directions — the effective role is this one whenever the association exists, higher OR lower — and two movements are refused with 403: downgrading a workspace |
{- "userId": "3f1b2c8e-5a4d-4b7e-9c10-2d6f8a1b4c33",
- "role": "developer"
}{- "workspaceId": "01JC4Z0000WORKSPACE00000001",
- "userId": "01JC4Z0000USUARIO0000000002",
- "role": "maintainer",
- "createdAt": "2026-07-22T08:00:00.000Z"
}Only owner, like the upsert. Removes the workspace row AND, in the same transaction, the user's rows in every project of this workspace (the project role overrides the workspace one, so leaving them would keep the user inside those projects), plus the user's PROJECT device keys and personal access tokens in them. Afterwards the live runner connection of that user in each project is dropped, best effort. Machine device keys and open sessions are left alone — they stop reaching this workspace because the role resolves to none.
Removing YOURSELF is refused with 403 and cannot be enabled anywhere: there is no level above to catch the fall. That same clause is what protects the last owner — only an owner can call this route and nobody removes themselves, so every successful call leaves at least the caller as owner. Removing ANOTHER owner is allowed — it is how ownership is revoked — except the OWNER OF RECORD (createdBy), refused with 409 criador_do_workspace until PUT :workspaceId/owner-of-record moves it to another owner: the LLM key agents spend and the spend report are theirs. Idempotent: a target without a workspace row still has this workspace's project rows cleared, and gets 204.
| workspaceId required | string |
| userId required | string |
The owner of record is createdBy: whose LLM credential the agents spend and whose git credential agent actions use (RN-058), and who owns the spend report (RN-060). It does not grant authorization — roles do. The target must already be an owner of the workspace (409 titular_precisa_ser_owner otherwise). From the commit on, agent turns look up the NEW owner of record's credential; if they have none for the model provider, the turn ends with the existing "no credential registered" outcome for that provider — this route does not check it beforehand. Any owner may call it, including for another owner. Transferring to the current owner of record is a no-op.
| workspaceId required | string |
| userId required | string <uuid> Id of the user who becomes the owner of record. Must already be an |
{- "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"
}The folder picker for Mounted mode, served by the api instead of by the runner (RN-504). Scoped HARD to BRABO_PROJECTS_BASE (ADR 0141): path is optional and defaults to the base itself, and any path outside it is a 400 — a malformed request, not a permission problem, since no role browses outside the base.
Capped on purpose: directories only, at most 500 of them (sorted BEFORE the cut, with truncado saying so), never recursive, dot-prefixed entries excluded, and symlinks reported but never followed. What is left out is COUNTED (arquivos, simbolicos) so a folder full of code never looks empty.
There is no POST companion: creating a folder belongs to materializing the mounted workspace, never to the picker.
| path | string Absolute path to list. Omitted means the base itself. Must be inside |
{- "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. Unique within the workspace. |
| maxConsecutiveBlocked | number Circuit breaker (Phase 12b — RN-047): how many consecutive tasks ending blocked stop the module's dev agent at idle_tripped. Omitted uses the domain default (DEFAULT_MAX_CONSECUTIVE_BLOCKED). Applies from the NEXT execution activation onward — doesn't affect agents already running. |
| storyPromotion | string Enum: "manual" "auto" Who promotes a story from |
| executionMode | string Enum: "container" "mounted" "runner" WHERE this project's command EXECUTES (RN-169/RN-421 — ADR 0072/0104). |
| workspacePath | string The folder's ABSOLUTE path, required when |
| language | string <= 35 characters The PROJECT's language (RN-619) — shared artifacts and turns with no human author use it. Any BCP-47 code the server recognizes, stored canonical; an unrecognized one is a 400. Omitted, the project starts with its creator's effective response language (RN-618: account choice > confirmed detection > interface language). |
{- "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"
}The single folder on the operator machine that the api and engine containers can see, mounted by identity (ADR 0141). null — a normal state, never an error — means this installation has no BRABO_PROJECTS_BASE, so the project wizard must not offer Mounted mode at all. The same value for every workspace: it is installation configuration, and workspaceId only scopes the authorization.
brokerConfigurado answers the other half of the same question: container and mounted only start a container through the broker (ADR 0144), so without it the wizard does not pre-select Mounted and offers Runner instead (ADR 0161, RN-573).
{- "projectsBase": "/home/voce/brabo",
- "brokerConfigurado": false
}One row per workspace project with a blocked task — feeds the dashboard sidebar's status dot with a single query instead of N calls to the backlog.
| workspaceId required | string |
[- {
- "projectId": "01JC4Z0000PROJETO0000000001",
- "blockedTaskCount": 2
}
]One row per workspace project with git provider, provisioning status, budget, last activity, stories awaiting promotion, and the facts that decide the agent roster (RN-090).
Same idea as projects-status, one floor up: the card grid is a WORKSPACE read, not N project reads. The previous path made seven polled queries per card, and with 23 projects the dashboard alone blew through the 300 req/min rate limit.
| 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
}
}
]Project count, agents that spent tokens this month, and this month's spend — feeds the summary line of the project dashboard.
| workspaceId required | string |
{- "activeProjects": 4,
- "agentCount": 6,
- "spentMicros": 12500000
}On by default. When on AND the turn model is from OpenRouter, an agent step with two or more tools first asks the Jev which tool fits, and the chat model is offered only that tool and the previous one. The Jev never approves or denies anything: an action that needs approval still does. Any failure of the Jev falls back to the whole catalog. Turning it off stops every call to the Jev, and its cost with them.
| workspaceId required | string |
| enabled required | boolean Whether the Jev chooses the tool of each agent step. On by default; it only acts when the turn model is from 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"
}The bell's drawer, for the whole workspace (RN-091). Each cursor says up to which seq that project has already been read, and the response carries what came after that in each one's most recent session.
It's POST because it's the only verb with a BODY, not because it changes state. The server doesn't know where each reader stopped: "read up to here" is a seq per project stored in the browser, and there is (deliberately) no mark-as-read endpoint. So the cutoff needs to TRAVEL in the request, and there are dozens of pairs — in a query string that becomes a long URL, which a proxy truncates, and would also put the user's project ids in access logs. The route is idempotent and has no side effect: it responds 200, never 201.
Replaces one request PER PROJECT with the drawer open — 23 projects cost 286 req/min against a limit of 300.
| workspaceId required | string |
required | Array of objects (UnreadCursorDto) <= 200 items One cursor per project to query. An EMPTY list returns an empty list — "I asked for nothing" is not "give me everything". A project from another workspace is silently ignored, because the cursor comes from the caller's local storage and may carry leftovers from an old workspace. | ||||
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"
}
]
}
]