Skip to main content

Brabo API (0.1.0)

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).

Authentication

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.

Errors

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.

Rate limit

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.

auth

Public access token verification keys

One key in normal operation; two during an AUTH_JWT_SECRET rotation.

Responses

Response samples

Content type
application/json
{
  • "keys": [
    ]
}

Authenticates and issues the access + refresh pair

A nonexistent email, wrong password, and a locked account all return exactly the same 401 response — same body, same status.

Request Body schema: application/json
required
email
required
string
senha
required
string

Responses

Request samples

Content type
application/json
{
  • "email": "fulano@brabo.dev",
  • "senha": "correct horse battery staple"
}

Response samples

Content type
application/json
{
  • "accessToken": "string",
  • "expiresIn": 900,
  • "locale": "pt-BR"
}

Revokes the family of the presented refresh

Always 204, even for an unknown token — answering 401 here would be a token-validity oracle.

Responses

Receives the social login OAuth callback

Success goes to WEB_ORIGIN/ already with the session cookies set; failure goes to WEB_ORIGIN/login?oauth_error=1, without detailing the reason.

path Parameters
provider
required
string
Enum: "github" "gitlab"
query Parameters
code
required
string
state
required
string

Responses

Redirects to the OAuth provider for social login

The state goes signed by HMAC with its OWN purpose — never the git connection-to-project flow's (see ADR 0084).

path Parameters
provider
required
string
Enum: "github" "gitlab"

Responses

Rotates the refresh and issues a new pair

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.

Responses

Response samples

Content type
application/json
{
  • "accessToken": "string",
  • "expiresIn": 900,
  • "locale": "pt-BR"
}

Creates an account and sends the verification email

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.

Request Body schema: application/json
required
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

Responses

Request samples

Content type
application/json
{
  • "email": "fulano@brabo.dev",
  • "senha": "correct horse battery staple",
  • "nome": "Fulano de Tal"
}

Response samples

Content type
application/json
{
  • "message": "If the address is available, we've sent an email."
}

Requests the password reset link

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.

Request Body schema: application/json
required
email
required
string

Responses

Request samples

Content type
application/json
{
  • "email": "fulano@brabo.dev"
}

Response samples

Content type
application/json
{
  • "message": "If the address is available, we've sent an email."
}

Sets the new password from the token

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.

Request Body schema: application/json
required
token
required
string

Single-use token received by email.

novaSenha
required
string >= 12 characters

Responses

Request samples

Content type
application/json
{
  • "token": "string",
  • "novaSenha": "another pretty long passphrase"
}

Confirms the email with the single-use token

Request Body schema: application/json
required
token
required
string

Single-use token received by email.

Responses

Request samples

Content type
application/json
{
  • "token": "string"
}

gates

The registry of active gates

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.

Authorizations:
bearer

Responses

Response samples

Content type
application/json
{
  • "version": 1,
  • "gates": [
    ]
}

Resumes a parked gate cycle

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.

Authorizations:
bearer
path Parameters
projectId
required
string
taskId
required
string
gate
required
string

Responses

Response samples

Content type
application/json
{
  • "ok": true
}

git

Receives the OAuth return and redirects to the web app

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.

path Parameters
provider
required
string
Enum: "github" "gitlab"
query Parameters
code
required
string
state
required
string

Responses

Annotates every line of a file with the commit that touched it

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.

Authorizations:
bearer
path Parameters
projectId
required
string
query Parameters
path
required
string

Path of the file to annotate.

ref
string

Branch, tag, or sha. Absent uses the repository's default branch.

Responses

Response samples

Content type
application/json
{
  • "ref": "dev",
  • "path": "apps/api/src/main.ts",
  • "lines": [
    ],
  • "truncated": false
}

Lists branches with ahead/behind and the associated open PR

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.

Authorizations:
bearer
path Parameters
projectId
required
string

Responses

Response samples

Content type
application/json
{
  • "items": [
    ],
  • "truncated": false
}

Returns a file's content

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.

Authorizations:
bearer
path Parameters
projectId
required
string
query Parameters
path
required
string

File path.

ref
string

Branch, tag, or sha.

Responses

Response samples

Content type
application/json
{
  • "ref": "dev",
  • "path": "apps/api/src/main.ts",
  • "content": "import { NestFactory } from '@nestjs/core';\n",
  • "truncated": false,
  • "bytes": 1284
}

Lists the repository's PRs/MRs

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.

Authorizations:
bearer
path Parameters
projectId
required
string
query Parameters
state
string
Enum: "open" "merged" "closed"

Absent lists all states.

Responses

Response samples

Content type
application/json
{
  • "items": [
    ],
  • "truncated": false
}

Returns the diff of a PR, normalized across providers

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.

Authorizations:
bearer
path Parameters
projectId
required
string
pullRequestId
required
string

Responses

Response samples

Content type
application/json
{
  • "pullRequestId": "pr-1",
  • "files": [
    ],
  • "truncated": false
}

Lists ONE level of the repository tree

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.

Authorizations:
bearer
path Parameters
projectId
required
string
query Parameters
ref
string

Branch, tag, or sha. Absent uses the repository's default branch.

path
string

Directory to list. Absent or empty is the root.

Responses

Response samples

Content type
application/json
{
  • "ref": "dev",
  • "path": "apps/api",
  • "entries": [
    ],
  • "truncated": false
}

Starts OAuth with the git provider

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.

Authorizations:
bearer
path Parameters
projectId
required
string
provider
required
string
Enum: "github" "gitlab"

Responses

Response samples

Creates the repository and runs the Gitflow bootstrap

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.

Authorizations:
bearer
path Parameters
projectId
required
string
provider
required
string
Enum: "local" "github" "gitlab"
Request Body schema: application/json
required
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.

Responses

Request samples

Content type
application/json
{
  • "name": "checkout",
  • "visibility": "private",
  • "namespace": "acme"
}

Response samples

Content type
application/json
{
  • "repository": {
    },
  • "bootstrap": {
    }
}

Adopts an existing repository, without creating anything

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).

Authorizations:
bearer
path Parameters
projectId
required
string
provider
required
string
Enum: "local" "github" "gitlab"
Request Body schema: application/json
required
externalId
required
string

The repository's identifier ON THE PROVIDER: owner/repo on github, namespace/path on gitlab, the bare repo's absolute path locally. Nothing is created — the repository must already exist and the registered credential must be able to reach it.

Responses

Request samples

Content type
application/json
{
  • "externalId": "acme/checkout"
}

Response samples

Content type
application/json
{
  • "repository": {
    },
  • "plan": {
    },
  • "alreadyAdopted": false
}

Returns the Gitflow bootstrap state

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.

Authorizations:
bearer
path Parameters
projectId
required
string

Responses

Response samples

Content type
application/json
{
  • "status": "provisioning",
  • "sessionId": "01JC4Z8QK3M7YV2N5T9B0PXHRA",
  • "failedStep": null,
  • "lastError": null,
  • "attempts": 1
}

Acknowledges the branch-protection failure and moves on

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.

Authorizations:
bearer
path Parameters
projectId
required
string

Responses

Response samples

Content type
application/json
{
  • "status": "provisioned"
}

Returns the bootstrap plan and what was decided about it

decision: null with a plan present is the state where NOTHING runs — waiting for approval or as-is adoption (RN-045).

Authorizations:
bearer
path Parameters
projectId
required
string

Responses

Response samples

Content type
application/json
{
  • "plan": {
    },
  • "decision": null,
  • "decidedAt": null,
  • "decidedBy": null
}

Approves the WHOLE plan and runs the bootstrap

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.

Authorizations:
bearer
path Parameters
projectId
required
string
Request Body schema: application/json
required
planGeneratedAt
required
string <date-time>

The generatedAt of the plan you SAW. Optimistic lock: if the plan has been regenerated since then (re-adoption, or the repository changed), the decision is refused with 409 instead of applying a yes given about something else.

Responses

Request samples

Content type
application/json
{
  • "planGeneratedAt": "2026-08-01T23:45:00.000Z"
}

Response samples

Content type
application/json
{
  • "repository": {
    },
  • "bootstrap": {
    }
}

Adopts the repository AS IS, dismissing the bootstrap

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.

Authorizations:
bearer
path Parameters
projectId
required
string
Request Body schema: application/json
required
planGeneratedAt
required
string <date-time>

The generatedAt of the plan you SAW. Optimistic lock: if the plan has been regenerated since then (re-adoption, or the repository changed), the decision is refused with 409 instead of applying a yes given about something else.

Responses

Request samples

Content type
application/json
{
  • "planGeneratedAt": "2026-08-01T23:45:00.000Z"
}

Response samples

Content type
application/json
{
  • "repository": {
    },
  • "bootstrap": {
    }
}

Returns the project's provisioned repository

Authorizations:
bearer
path Parameters
projectId
required
string

Responses

Response samples

Content type
application/json
{
  • "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"
}

infrastructure

Readiness — the process can actually serve traffic

Runs a select 1. Reveals only whether the database responds; no topology or version. Public because the caller is kubelet, which carries no token.

Responses

Response samples

Content type
application/json
{
  • "service": "api",
  • "status": "ok",
  • "timestamp": "2026-07-27T15:40:00.000Z",
  • "details": { }
}

Liveness — the process still serves requests

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.

Responses

Response samples

Content type
application/json
{
  • "service": "api",
  • "status": "ok",
  • "timestamp": "2026-07-27T15:40:00.000Z",
  • "details": { }
}

Exposes metrics in Prometheus exposition format

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.

Responses

Response samples

Content type
text/plain
# HELP oban_queue_depth Oban queue depth
# TYPE oban_queue_depth gauge
oban_queue_depth{queue="default",state="available"} 0

Downloads the standalone local runner binary for the requested platform

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.

Responses

internal

Creates the FIRST account of an installation, already verified

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.

Authorizations:
service-token
Request Body schema: application/json
required
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 .env, to the marker or to a log, and never generated by any code: there is no path where an account is born with a password the script chose. The minimum is the DOMAIN policy, the same code registration uses; the max length here is argon2id protection, not policy.

nome
string

Display name. Omitted: the personal workspace's name falls back to the e-mail local-part (RN-410) — nothing is invented.

Responses

Request samples

Content type
application/json
{
  • "email": "voce@exemplo.dev",
  • "senha": "uma frase longa e minha",
  • "nome": "Fulana de Tal"
}

Response samples

Content type
application/json
{
  • "userId": "01JC4Z0000USUARIO0000000001",
  • "email": "voce@exemplo.dev",
  • "workspaceId": "01JC4Z0000WORKSPACE00000001"
}

Reads the declarative gate registry

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.

Authorizations:
service-token

Responses

Response samples

Content type
application/json
{
  • "version": 1,
  • "gates": [
    ]
}

Publishes a prompt template version, idempotent by hash

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).

Authorizations:
service-token
Request Body schema: application/json
required
name
required
string
version
required
string
body
required
string
hash
required
string

Responses

Request samples

Content type
application/json
{
  • "name": "dev-agent-kickoff",
  • "version": "3",
  • "body": "You are the dev agent for the {{modulo}} module...",
  • "hash": "sha256:9f2c..."
}

Response samples

Content type
application/json
{
  • "name": "dev-agent-kickoff",
  • "version": "3",
  • "body": "You are the dev agent for the {{modulo}} module...",
  • "hash": "sha256:9f2c...",
  • "active": true
}

Looks up a prompt template by name

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.

Authorizations:
service-token
path Parameters
name
required
string
query Parameters
version
string
Example: version=3

Responses

Response samples

Content type
application/json
{
  • "name": "dev-agent-kickoff",
  • "version": "3",
  • "body": "You are the dev agent for the {{modulo}} module...",
  • "hash": "sha256:9f2c..."
}

Registers the MACHINE device key of a fresh installation

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.

Authorizations:
service-token
Request Body schema: application/json
required
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 d is refused with 400 saying so, never stored.

Responses

Request samples

Content type
application/json
{
  • "name": "servidor-de-casa",
  • "publicKeyJwk": "{\"kty\":\"OKP\",\"crv\":\"Ed25519\",\"x\":\"…\"}"
}

Response samples

Content type
application/json
{
  • "id": "01JC4Z0000CHAVE000000000001",
  • "userId": "01JC4Z0000USUARIO0000000001",
  • "name": "servidor-de-casa",
  • "createdAt": "2026-09-12T12:00:00.000Z",
  • "replacedKeyIds": [ ]
}

Syncs the model catalog across all providers

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.

Authorizations:
service-token

Responses

Response samples

Content type
application/json
{
  • "porProvider": [
    ]
}

The project's backlog as a tree, for the PO to read what was already written

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).

Authorizations:
service-token
path Parameters
projectId
required
string

Responses

Response samples

Content type
application/json
[
  • {
    }
]

The project's business rules, with coverage, for the PO to read

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.

Authorizations:
service-token
path Parameters
projectId
required
string

Responses

Response samples

Content type
application/json
{
  • "rules": [
    ],
  • "uncoveredCount": 2
}

Runs a terminal command INSIDE the project real container (RN-492)

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.

Authorizations:
service-token
path Parameters
projectId
required
string
Request Body schema: application/json
required
comando
required
string

The shell command, interpreted by sh -c INSIDE the container — same convention as the terminal action run outside one.

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 /work, the project root. The broker re-validates it is inside /work and refuses (DiretorioForaDoEscopoError) otherwise.

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.

Responses

Request samples

Content type
application/json
{
  • "comando": "npm test",
  • "cwd": "/work/.worktrees/dev-api",
  • "timeoutMs": 300000,
  • "alvo": {
    }
}

Response samples

Content type
application/json
{
  • "sucesso": true,
  • "exitCode": 0,
  • "output": "ok\n",
  • "timedOut": false,
  • "motivo": "o broker de container não respondeu em http://broker:8090: timeout"
}

What the broker needs to compose the container spec itself

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.

Authorizations:
service-token
path Parameters
projectId
required
string
query Parameters
papel
string
Value: "qa"
task
string

Responses

Response samples

Content type
application/json
{
  • "projectId": "f52be111-0000-4000-8000-000000000000",
  • "projectSlug": "exp002",
  • "workspaceId": "aaaaaaaa-0000-4000-8000-000000000000",
  • "workspaceDirName": "exp002-f52be111",
  • "executionMode": "container",
  • "localizacao": {
    },
  • "usuarioDaPasta": {
    },
  • "imagem": {
    },
  • "imagemVersao": 3,
  • "comandoDoApp": "string",
  • "ports": [
    ],
  • "dns": [
    ],
  • "extraHosts": [
    ],
  • "qa": {
    }
}

The project's working remote, to fetch and push

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.

Authorizations:
service-token
path Parameters
projectId
required
string

Responses

Response samples

Content type
application/json
{}

The local agent reports the outcome of a mirror round (RN-517)

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.

Authorizations:
service-token
path Parameters
projectId
required
string
Request Body schema: application/json
required
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. 0 is a number, not an absence — "synced and copied nothing" is a state of its own (RN-088).

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.

Responses

Request samples

Content type
application/json
{
  • "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"
}

Response samples

Content type
application/json
{
  • "recorded": true,
  • "status": "synced"
}

The contracts between modules, for a dev agent to read (RN-684)

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.

Authorizations:
service-token
path Parameters
projectId
required
string

Responses

Response samples

Content type
application/json
{
  • "status": "sem_contratos",
  • "version": 2,
  • "modulos": [
    ],
  • "contratosForaDoMapa": [ ]
}

The project's delivery funnel and partial DORA metrics, for the PO to read

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).

Authorizations:
service-token
path Parameters
projectId
required
string

Responses

Response samples

Content type
application/json
{
  • "project": {
    },
  • "totalActionsConsidered": 18,
  • "funnel": {
    },
  • "leadTimes": {
    },
  • "deploymentFrequency": [
    ]
}

Removes the QA instance of a task (RN-936)

Automatic, no proposal: it only reaches what carries the QA label (the broker guarantees it). Idempotent.

Authorizations:
service-token
path Parameters
projectId
required
string
taskId
required
string

Responses

Response samples

Content type
application/json
{
  • "sucesso": true,
  • "status": "provisioning",
  • "chave": "a1b2c3d4",
  • "motivo": "string"
}

Brings up the ephemeral QA instance of a task (RN-935)

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.

Authorizations:
service-token
path Parameters
projectId
required
string
taskId
required
string

Responses

Response samples

Content type
application/json
{
  • "sucesso": true,
  • "status": "provisioning",
  • "chave": "a1b2c3d4",
  • "motivo": "string"
}

The runner confirms the path of a "runner" project (RN-423)

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.

Authorizations:
service-token
path Parameters
projectId
required
string
Request Body schema: application/json
required
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 (ProjectSession.latest_id/1), resolved on the engine side. null/omitted when the project doesn't have any session yet — the project is still updated, only the audit event is skipped (accepted gap, RN-423).

actorId
string <uuid>

Who requested the runner ticket (owner of the socket that confirmed) — becomes the actor of the project.workspace_verified event. Omitted: the event (if there is a session) is born with no identified actor.

Responses

Request samples

Content type
application/json
{
  • "path": "/home/you/projects/store",
  • "sessionId": "01JC4Z0000SESSAO00000000001",
  • "actorId": "01JC4Z0000USUARIO0000000001"
}

Response samples

Content type
application/json
{
  • "verified": true,
  • "workspacePath": "/home/you/projects/store",
  • "changed": true
}

Removes QA instances whose gate is no longer in flight (RN-937)

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).

Authorizations:
service-token
Request Body schema: application/json
required
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.

Responses

Request samples

Content type
application/json
{
  • "taskIdsEmVoo": [
    ],
  • "projectId": "5a8591dd-4039-49df-9202-96385ba3eff8"
}

Response samples

Content type
application/json
{
  • "removidas": [
    ],
  • "falhas": [
    ]
}

The agent's vote on one retrieved chunk (RN-480)

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.

Authorizations:
service-token
Request Body schema: application/json
required
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.

Responses

Request samples

Content type
application/json
{
  • "projectId": "5a8591dd-4039-49df-9202-96385ba3eff8",
  • "searchId": "9b1a67f9-9477-48e5-8a6c-11b70245e1d9",
  • "chunkId": "90e1f3df-a8a1-438e-8dd5-10e45398d051",
  • "verdict": "util",
  • "agent": "qa"
}

Response samples

Content type
application/json
{
  • "searchId": "9b1a67f9-9477-48e5-8a6c-11b70245e1d9",
  • "chunkId": "90e1f3df-a8a1-438e-8dd5-10e45398d051",
  • "verdict": "util",
  • "rank": 0
}

Hybrid search over the project's RAG index, for the engine's tool

Same engine as POST /projects/:projectId/rag/search (human route), projected into the format the engine's rag_search tool expects.

Authorizations:
service-token
Request Body schema: application/json
required
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 rag.search timeline narration is skipped. Never invented by the api: a session it did not receive is a session that did not exist.

agent
string

Agent slug, recorded as the telemetry actor (actor_kind: agent). Absent falls back to the system actor rather than inventing a user.

Responses

Request samples

Content type
application/json
{
  • "projectId": "5a8591dd-4039-49df-9202-96385ba3eff8",
  • "query": "how the parallelism cap is decided",
  • "topK": 5,
  • "sessionId": "f6567dd8-e069-418e-8893-7d22fcf12459",
  • "agent": "dev-lead"
}

Response samples

Content type
application/json
{
  • "searchId": "9b1a67f9-9477-48e5-8a6c-11b70245e1d9",
  • "hits": [
    ],
  • "degraded": true
}

Proposes an action with an external effect on behalf of an agent

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.

Authorizations:
service-token
path Parameters
sessionId
required
string
Request Body schema: application/json
required
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

Responses

Request samples

Content type
application/json
{
  • "projectId": "01JC4Z0000PROJETO0000000001",
  • "actionType": "terminal",
  • "actor": {
    },
  • "payload": {
    }
}

Response samples

Content type
application/json
{
  • "id": "01JC4Z8QK3M7YV2N5T9B0PXHRC",
  • "projectId": "01JC4Z0000PROJETO0000000001",
  • "sessionId": "01JC4Z8QK3M7YV2N5T9B0PXHRA",
  • "seq": 7,
  • "actionType": "terminal",
  • "payload": {
    },
  • "status": "pending",
  • "resolvedPolicy": "require_approval",
  • "actor": {
    },
  • "decidedBy": null,
  • "decidedAt": null,
  • "rejectionReason": null,
  • "executionResult": null,
  • "createdAt": "2026-07-27T14:33:10.900Z",
  • "updatedAt": "2026-07-27T14:33:10.900Z"
}

Assembles the context for an Anamnesis round

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.

Authorizations:
service-token
query Parameters
projectId
required
string

Responses

Response samples

Content type
application/json
{
  • "competencyCatalog": [
    ],
  • "members": [
    ],
  • "queuedHypotheses": [
    ],
  • "currentProfiles": [
    ],
  • "instructions": [
    ],
  • "decisions": [
    ],
  • "windowFrom": "2026-07-27T12:00:00.000Z"
}

Closes the Anamnese window of a round that emitted no profile

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.

Authorizations:
service-token
path Parameters
sessionId
required
string
Request Body schema: application/json
required
projectId
required
string <uuid>
windowFrom
required
string <date-time>
windowTo
required
string <date-time>
eventCount
required
number

Responses

Request samples

Content type
application/json
{
  • "projectId": "01JC4Z0000PROJETO0000000001",
  • "windowFrom": "2026-07-27T12:00:00.000Z",
  • "windowTo": "2026-07-27T12:15:00.000Z",
  • "eventCount": 12
}

Response samples

Content type
application/json
{
  • "runId": "01JC4Z0000RODADA00000000001",
  • "profiles": [
    ]
}

Generates a new version of the C4 diagram (Context + Container)

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).

Authorizations:
service-token
path Parameters
sessionId
required
string
Request Body schema: application/json
required
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.

Responses

Request samples

Content type
application/json
{
  • "projectId": "5a8591dd-4039-49df-9202-96385ba3eff8",
  • "systemName": "Brabo",
  • "systemDescription": "Agent-orchestrated engineering platform.",
  • "actors": [
    ]
}

Response samples

Content type
application/json
{
  • "diagrama": {
    },
  • "version": 1
}

Records the outcome of an area delegation

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.

Authorizations:
service-token
path Parameters
sessionId
required
string
Request Body schema: application/json
required
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 artifact.qa_verdict session_event for THIS subspecialty's VERDICT — required when status is "completed".

failureOrigin
string
Enum: "infra" "modelo" "codigo" "politica"

The ORIGIN of the failure (ADR 0020/0038), never by elimination — required when status is "failed".

failureReason
string
justification
string

Why the lead chose NOT to delegate — required when status is "dispensed". Never left blank: dispensing is a recorded decision, not silence (CLAUDE.md, Phase 8b item 2).

Responses

Request samples

Content type
application/json
{
  • "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"
}

Response samples

Content type
application/json
{
  • "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"
}

Assembles the full context of a task for the dev agent

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.

Authorizations:
service-token
query Parameters
projectId
required
string
taskId
required
string
module
string
Example: module=api

Responses

Response samples

Content type
application/json
{
  • "task": {
    },
  • "story": {
    },
  • "businessRules": [
    ],
  • "adrs": [
    ],
  • "siblingTasks": [
    ],
  • "moduleOpenTasks": [
    ],
  • "moduleOpenTasksTotal": 0,
  • "moduleContract": {
    },
  • "primeiraDoModulo": true,
  • "decisoes": [
    ],
  • "decisoesDoProjeto": [
    ]
}

Creates a backlog epic

Authorizations:
service-token
path Parameters
sessionId
required
string
Request Body schema: application/json
required
projectId
required
string <uuid>
title
required
string
description
string

Responses

Request samples

Content type
application/json
{
  • "projectId": "01JC4Z0000PROJETO0000000001",
  • "title": "Shopping cart",
  • "description": "Everything the buyer does before paying."
}

Response samples

Content type
application/json
{
  • "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"
}

Paginates the session's event log for the engine

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).

Authorizations:
service-token
path Parameters
sessionId
required
string
query Parameters
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 afterSeq.

types
string
Example: types=artifact.product_brief,artifact.business_rule

Comma-separated event types (at most 20). Only events of these types count toward limit.

Responses

Response samples

Content type
application/json
{
  • "items": [
    ],
  • "nextCursor": 41
}

Appends an event to the session's log, on behalf of an agent

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.

Authorizations:
service-token
path Parameters
sessionId
required
string
Request Body schema: application/json
required
projectId
required
string <uuid>
type
required
string

See docs/reference/events.md.

actorKind
required
string
Enum: "user" "agent" "system"
actorId
required
string

Slug of the agent that emitted it.

required
object

Responses

Request samples

Content type
application/json
{
  • "projectId": "01JC4Z0000PROJETO0000000001",
  • "type": "agent.tool_call",
  • "actorKind": "agent",
  • "actorId": "dev-api",
  • "payload": {
    }
}

Response samples

Content type
application/json
{
  • "id": "01JC4Z8QK3M7YV2N5T9B0PXHRB",
  • "sessionId": "01JC4Z8QK3M7YV2N5T9B0PXHRA",
  • "seq": 41,
  • "type": "chat.message",
  • "actor": {
    },
  • "payload": {
    },
  • "createdAt": "2026-07-27T14:30:58.512Z"
}

Records QA's or SecOps's verdict on a task's PR

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.

Authorizations:
service-token
path Parameters
sessionId
required
string
Request Body schema: application/json
required
projectId
required
string <uuid>
taskId
required
string <uuid>
gate
required
string
Enum: "qa" "secops"
veredito
required
string
Enum: "approved" "changes_requested" "needs_user_decision"

needs_user_decision (QA only, RN-860): the cycle waits for the user's answer to an open question — it does not count as a correction.

resumo
required
string
itens
required
Array of strings

What needs to change, item by item.

pergunta
string

With needs_user_decision: the new open question (up to 500 chars).

sugestao
string

With needs_user_decision: the agent's suggestion.

openQuestionId
string <uuid>

With needs_user_decision: an EXISTING open question instead of a new one.

maxCorrections
number

Cap on round trips before the task becomes blocked. Omitted uses the domain's default.

Responses

Request samples

Content type
application/json
{
  • "projectId": "01JC4Z0000PROJETO0000000001",
  • "taskId": "01JC4Z0000TAREFA00000000001",
  • "gate": "qa",
  • "veredito": "changes_requested",
  • "resumo": "Missing tests for the insufficient-stock path.",
  • "itens": [
    ],
  • "pergunta": "Should a nonexistent code answer 404?",
  • "sugestao": "404 with a plain message.",
  • "openQuestionId": "4c8aa973-c398-4859-8f2e-3fe00dc96a12",
  • "maxCorrections": 3
}

Response samples

Content type
application/json
{
  • "nextAction": "run_secops",
  • "task": {
    },
  • "openQuestionId": "4c8aa973-c398-4859-8f2e-3fe00dc96a12"
}

Offers a handoff from one agent to another

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.

Authorizations:
service-token
path Parameters
sessionId
required
string
Request Body schema: application/json
required
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 (desfecho: ja_oferecido) instead of replacing it. AppSec sends it.

Responses

Request samples

Content type
application/json
{
  • "projectId": "01JC4Z0000PROJETO0000000001",
  • "fromAgent": "criativo",
  • "toAgent": "po",
  • "artifactId": "01JC4Z8QK3M7YV2N5T9B0PXHRB",
  • "seAusente": true
}

Response samples

Content type
application/json
{
  • "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": {
    }
}

Records an analysis round and its hypotheses

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.

Authorizations:
service-token
path Parameters
sessionId
required
string
Request Body schema: application/json
required
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 terminationAnalysis is required. Optional so as not to break an older engine during a rolling deploy.

required
Array of objects (HypothesisDraftDto)

Responses

Request samples

Content type
application/json
{
  • "projectId": "01JC4Z0000PROJETO0000000001",
  • "tier": "leve",
  • "triggeredBy": "auto",
  • "eventCount": 128,
  • "cause": "timeout",
  • "hypotheses": [
    ]
}

Response samples

Content type
application/json
{
  • "analysisId": "01JC4Z0000ANALISE0000000001",
  • "hypotheses": [
    ]
}

Returns the files of an infra PR for the gates to read

The content comes from the proposed_action's own payload: an infra artifact never touches a worktree, same as ADRs.

Authorizations:
service-token
path Parameters
prActionId
required
string
query Parameters
projectId
required
string

Responses

Response samples

Content type
application/json
{
  • "title": "api's production Dockerfile",
  • "files": [
    ]
}

Assembles the InfraAgent's initial context

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.

Authorizations:
service-token
query Parameters
projectId
required
string

Responses

Response samples

Content type
application/json
{
  • "moduleMap": {
    },
  • "adrs": [
    ],
  • "gitProvider": "github",
  • "moduleRouting": {
    },
  • "projectImage": {
    }
}

Records QA's or SecOps's verdict on an infra PR

Same pipeline and same state machine as dev PRs.

Authorizations:
service-token
path Parameters
sessionId
required
string
Request Body schema: application/json
required
projectId
required
string <uuid>
prActionId
required
string <uuid>

The open_infra_pr proposed_action that opened the PR.

gate
required
string
Enum: "qa" "secops"
veredito
required
string
Enum: "approved" "changes_requested"
resumo
required
string
itens
required
Array of strings
maxCorrections
number

Responses

Request samples

Content type
application/json
{
  • "projectId": "01JC4Z0000PROJETO0000000001",
  • "prActionId": "01JC4Z8QK3M7YV2N5T9B0PXHRD",
  • "gate": "secops",
  • "veredito": "approved",
  • "resumo": "Non-root image with no secret in ARG.",
  • "itens": [ ],
  • "maxCorrections": 3
}

Response samples

Content type
application/json
{
  • "nextAction": "done",
  • "artifact": {
    }
}

Proposes a patch to an agent's instruction file

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.

Authorizations:
service-token
path Parameters
sessionId
required
string
Request Body schema: application/json
required
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.

Responses

Request samples

Content type
application/json
{
  • "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"
}

Response samples

Content type
application/json
{
  • "id": "01JC4Z8QK3M7YV2N5T9B0PXHRC",
  • "projectId": "01JC4Z0000PROJETO0000000001",
  • "sessionId": "01JC4Z8QK3M7YV2N5T9B0PXHRA",
  • "seq": 7,
  • "actionType": "terminal",
  • "payload": {
    },
  • "status": "pending",
  • "resolvedPolicy": "require_approval",
  • "actor": {
    },
  • "decidedBy": null,
  • "decidedAt": null,
  • "rejectionReason": null,
  • "executionResult": null,
  • "createdAt": "2026-07-27T14:33:10.900Z",
  • "updatedAt": "2026-07-27T14:33:10.900Z"
}

Runs a metered LLM turn, with tool support

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.

Authorizations:
service-token
path Parameters
sessionId
required
string
Request Body schema: application/json
required
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; parameters is JSON Schema.

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.

Responses

Request samples

Content type
application/json
{
  • "projectId": "01JC4Z0000PROJETO0000000001",
  • "agentId": "dev-api",
  • "messages": [
    ],
  • "tools": [
    ],
  • "catalogoCompleto": true
}

Response samples

Content type
application/json
{
  • "message": {
    },
  • "usage": {
    },
  • "error": null,
  • "errorCode": null,
  • "modelName": "llama3.2:3b",
  • "truncated": true,
  • "toolRouting": {
    }
}

Runs an LLM turn with the response streamed

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.

Authorizations:
service-token
path Parameters
sessionId
required
string
Request Body schema: application/json
required
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.

Responses

Request samples

Content type
application/json
{
  • "projectId": "01JC4Z0000PROJETO0000000001",
  • "agentId": "criativo",
  • "messages": [
    ],
  • "tools": [
    ],
  • "catalogoCompleto": true
}

Proposes raising an area's parallelism cap

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.

Authorizations:
service-token
path Parameters
sessionId
required
string
Request Body schema: application/json
required
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.

Responses

Request samples

Content type
application/json
{
  • "projectId": "01JC4Z0000PROJETO0000000001",
  • "area": "dev",
  • "proposto": 4,
  • "rationale": "You authorized the same request four times in this window, and none was denied."
}

Response samples

Content type
application/json
{
  • "id": "01JC4Z8QK3M7YV2N5T9B0PXHRC",
  • "projectId": "01JC4Z0000PROJETO0000000001",
  • "sessionId": "01JC4Z8QK3M7YV2N5T9B0PXHRA",
  • "seq": 7,
  • "actionType": "terminal",
  • "payload": {
    },
  • "status": "pending",
  • "resolvedPolicy": "require_approval",
  • "actor": {
    },
  • "decidedBy": null,
  • "decidedAt": null,
  • "rejectionReason": null,
  • "executionResult": null,
  • "createdAt": "2026-07-27T14:33:10.900Z",
  • "updatedAt": "2026-07-27T14:33:10.900Z"
}

Declares a new version of the contracts between modules

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.

Authorizations:
service-token
path Parameters
sessionId
required
string
Request Body schema: application/json
required
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 dependsOn when read.

Responses

Request samples

Content type
application/json
{
  • "projectId": "5a8591dd-4039-49df-9202-96385ba3eff8",
  • "contratos": [
    ]
}

Response samples

Content type
application/json
{
  • "contratos": [
    ],
  • "version": 1
}

Publishes a new version of the module_map

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.

Authorizations:
service-token
path Parameters
sessionId
required
string
Request Body schema: application/json
required
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 resources (cpus, memoryMb, pidsLimit — all three or none); a partial declaration, a value above the container ceiling, or a SUM over modules above it is also 400 (RN-683).

Responses

Request samples

Content type
application/json
{
  • "projectId": "01JC4Z0000PROJETO0000000001",
  • "modules": [
    ]
}

Response samples

Content type
application/json
{
  • "id": "01JC4Z0000MODULEMAP00000001",
  • "projectId": "01JC4Z0000PROJETO0000000001",
  • "sessionId": "01JC4Z8QK3M7YV2N5T9B0PXHRA",
  • "modules": [
    ],
  • "version": 3,
  • "createdAt": "2026-07-24T16:00:00.000Z"
}

Routes each module of the current module_map to a candidate image

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.

Authorizations:
service-token
path Parameters
sessionId
required
string
Request Body schema: application/json
required
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.

Responses

Request samples

Content type
application/json
{
  • "projectId": "5a8591dd-4039-49df-9202-96385ba3eff8",
  • "roteamento": [
    ]
}

Response samples

Content type
application/json
{
  • "roteamento": [
    ],
  • "version": 1
}

Does the session have pending work that blocks closing it?

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.

Authorizations:
service-token
path Parameters
sessionId
required
string

Responses

Response samples

Content type
application/json
{
  • "pending": true,
  • "motivo": "handoff po → arquiteto aguardando aceite",
  • "aguardandoUsuarioDesde": "2026-09-18T12:00:00.000Z"
}

Records the proficiency profiles derived in the round

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.

Authorizations:
service-token
path Parameters
sessionId
required
string
Request Body schema: application/json
required
projectId
required
string <uuid>
windowFrom
required
string <date-time>
windowTo
required
string <date-time>
eventCount
required
number
required
Array of objects (ProficiencyDraftDto)

Responses

Request samples

Content type
application/json
{
  • "projectId": "01JC4Z0000PROJETO0000000001",
  • "windowFrom": "2026-07-27T12:00:00.000Z",
  • "windowTo": "2026-07-27T12:15:00.000Z",
  • "eventCount": 240,
  • "profiles": [
    ]
}

Response samples

Content type
application/json
{
  • "runId": "01JC4Z0000RODADA00000000001",
  • "profiles": [
    ]
}

Sets the project's container image

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.

Authorizations:
service-token
path Parameters
sessionId
required
string
Request Body schema: application/json
required
projectId
required
string <uuid>
image
required
string

OCI reference with an explicit TAG or digest. latest is REFUSED with 400 — the rule is domain-level.

rationale
required
string

Why this image. Minimum of 10 characters.

network
string
Default: "none"
Enum: "none" "egress"

The container's network posture. none is the default, and it is what makes "inside, the agent is free" a safe sentence.

object

Resource cap. Above the maximum, the decision is REFUSED, never silently downgraded.

Responses

Request samples

Content type
application/json
{
  • "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": {
    }
}

Response samples

Content type
application/json
{
  • "decisao": {
    },
  • "version": 1,
  • "containerDePe": {
    }
}

Assembles the context for a Psychologist round

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.

Authorizations:
service-token
path Parameters
sessionId
required
string
query Parameters
projectId
required
string

Responses

Response samples

Content type
application/json
{
  • "alreadyAnalyzed": false,
  • "sessionStatus": "closed_abnormally",
  • "terminationReason": "heartbeat_timeout",
  • "businessRules": [
    ],
  • "priorHypotheses": [
    ]
}

Warns when a just-written business rule looks like an existing one

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).

Authorizations:
service-token
path Parameters
sessionId
required
string
Request Body schema: application/json
required
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.

Responses

Request samples

Content type
application/json
{
  • "projectId": "01JC4Z0000PROJETO0000000001",
  • "kind": "business_rule",
  • "title": "Greeting with the caller name"
}

Response samples

Content type
application/json
{
  • "status": "warned",
  • "similarTo": {
    },
  • "closest": {
    },
  • "similarity": 0.83,
  • "threshold": 0.8,
  • "compared": 12,
  • "total": 12,
  • "reason": "provider \"ollama\" did not answer",
  • "message": "string"
}

Creates a story with functional/non-functional requirements, DoD, DoR, and covered rules

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.

Authorizations:
service-token
path Parameters
sessionId
required
string
Request Body schema: application/json
required
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 artifact.business_rule event that EXISTS — a made-up id is refused.

Responses

Request samples

Content type
application/json
{
  • "projectId": "01JC4Z0000PROJETO0000000001",
  • "epicId": "01JC4Z0000EPICO000000000001",
  • "title": "Add item to cart",
  • "description": "As a buyer, I want to gather items before paying.",
  • "rf": [
    ],
  • "rnf": [
    ],
  • "dod": [
    ],
  • "dor": [
    ],
  • "businessRuleIds": [
    ]
}

Response samples

Content type
application/json
{
  • "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": [
    ],
  • "rnf": [
    ],
  • "businessRuleIds": [
    ],
  • "dod": [
    ],
  • "dor": [
    ],
  • "moduleIds": [
    ],
  • "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": {
    }
}

Archives a draft story

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).

Authorizations:
service-token
path Parameters
storyId
required
string
Request Body schema: application/json
required
projectId
required
string <uuid>
reason
string

Responses

Request samples

Content type
application/json
{
  • "projectId": "01JC4Z0000PROJETO0000000001",
  • "reason": "Duplicate of \"Register a user\"."
}

Response samples

Content type
application/json
{
  • "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": [
    ],
  • "rnf": [
    ],
  • "businessRuleIds": [
    ],
  • "dod": [
    ],
  • "dor": [
    ],
  • "moduleIds": [
    ],
  • "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"
}

Completes an existing draft story instead of re-creating it

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).

Authorizations:
service-token
path Parameters
sessionId
required
string
storyId
required
string
Request Body schema: application/json
required
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 artifact.business_rule.

questionId
string <uuid>

RN-859 (ADR 0214): an open question the user ALREADY answered — only then a ready story (no task in execution) can be corrected.

Responses

Request samples

Content type
application/json
{
  • "projectId": "01JC4Z0000PROJETO0000000001",
  • "description": "As a buyer, I want to pay by card.",
  • "rf": [
    ],
  • "rnf": [
    ],
  • "dod": [
    ],
  • "dor": [
    ],
  • "businessRuleIds": [
    ],
  • "questionId": "ffa0420e-f70b-4e5b-99a0-434bff860467"
}

Response samples

Content type
application/json
{
  • "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": [
    ],
  • "rnf": [
    ],
  • "businessRuleIds": [
    ],
  • "dod": [
    ],
  • "dor": [
    ],
  • "moduleIds": [
    ],
  • "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"
}

Corrects the title and/or description of a draft story

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).

Authorizations:
service-token
path Parameters
storyId
required
string
Request Body schema: application/json
required
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 ready story (no task in execution) can be corrected.

Responses

Request samples

Content type
application/json
{
  • "projectId": "01JC4Z0000PROJETO0000000001",
  • "title": "Register a user",
  • "description": "As a visitor, I want to sign up.",
  • "questionId": "ffa0420e-f70b-4e5b-99a0-434bff860467"
}

Response samples

Content type
application/json
{
  • "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": [
    ],
  • "rnf": [
    ],
  • "businessRuleIds": [
    ],
  • "dod": [
    ],
  • "dor": [
    ],
  • "moduleIds": [
    ],
  • "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"
}

Associates a story with the modules it touches

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.

Authorizations:
service-token
path Parameters
sessionId
required
string
Request Body schema: application/json
required
projectId
required
string <uuid>
storyId
required
string <uuid>
moduleIds
required
Array of strings

Names from the current module_map. A module that doesn't exist becomes an architecture pending item.

Responses

Request samples

Content type
application/json
{
  • "projectId": "01JC4Z0000PROJETO0000000001",
  • "storyId": "01JC4Z0000HISTORIA000000001",
  • "moduleIds": [
    ]
}

Response samples

Content type
application/json
{
  • "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": [
    ],
  • "rnf": [
    ],
  • "businessRuleIds": [
    ],
  • "dod": [
    ],
  • "dor": [
    ],
  • "moduleIds": [
    ],
  • "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"
}

Creates a task inside a story

Authorizations:
service-token
path Parameters
sessionId
required
string
Request Body schema: application/json
required
projectId
required
string <uuid>
storyId
required
string <uuid>
title
required
string
description
string

Responses

Request samples

Content type
application/json
{
  • "projectId": "01JC4Z0000PROJETO0000000001",
  • "storyId": "01JC4Z0000HISTORIA000000001",
  • "title": "Endpoint POST /cart/items",
  • "description": "Accepts SKU and quantity; validates stock."
}

Response samples

Content type
application/json
{
  • "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"
}

Marks the task as blocked, with the reason

There is no automatic unblocking: whoever unblocks it is a person, via the human route. That is what stops an agent from spinning indefinitely.

Authorizations:
service-token
path Parameters
sessionId
required
string
taskId
required
string
Request Body schema: application/json
required
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: infra, modelo, codigo, or politica. Never by elimination — this is the lesson from ADR 0020, taken up a level by the QA Lead in ADR 0038. Optional: not every call site knows the origin.

Responses

Request samples

Content type
application/json
{
  • "projectId": "01JC4Z0000PROJETO0000000001",
  • "agentId": "dev-api",
  • "reason": "Gate correction cap reached.",
  • "diagnosis": "Correction cycle exhausted without exit 0.",
  • "origin": "infra"
}

Response samples

Content type
application/json
{
  • "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"
}

Opens the task's gate pipeline, starting with QA

Called when the dev agent's PR is ready for review.

Authorizations:
service-token
path Parameters
sessionId
required
string
taskId
required
string
Request Body schema: application/json
required
projectId
required
string <uuid>
agentId
required
string

Responses

Request samples

Content type
application/json
{
  • "projectId": "01JC4Z0000PROJETO0000000001",
  • "agentId": "dev-api"
}

Response samples

Content type
application/json
{
  • "gateStatus": "awaiting_qa"
}

Moves the task's state as work progresses

Authorizations:
service-token
path Parameters
sessionId
required
string
taskId
required
string
Request Body schema: application/json
required
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"

Responses

Request samples

Content type
application/json
{
  • "projectId": "01JC4Z0000PROJETO0000000001",
  • "agentId": "dev-api",
  • "status": "in_review"
}

Response samples

Content type
application/json
{
  • "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"
}

Unblocks a task after a transient network/provider failure

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.

Authorizations:
service-token
path Parameters
sessionId
required
string
taskId
required
string
Request Body schema: application/json
required
projectId
required
string <uuid>
agentId
required
string

The dev agent whose transient failure scheduled the retry.

Responses

Request samples

Content type
application/json
{
  • "projectId": "01JC4Z0000PROJETO0000000001",
  • "agentId": "dev-api"
}

Response samples

Content type
application/json
{
  • "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"
}

Atomically claims the module's next claimable task

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.

Authorizations:
service-token
path Parameters
sessionId
required
string
Request Body schema: application/json
required
projectId
required
string <uuid>
module
required
string

Only tasks from this module enter the draw.

agentId
required
string

Responses

Request samples

Content type
application/json
{
  • "projectId": "01JC4Z0000PROJETO0000000001",
  • "module": "api",
  • "agentId": "dev-api"
}

Response samples

Content type
application/json
{
  • "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"
}

Lists the project's in-progress tasks held by a dev agent

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.

Authorizations:
service-token
path Parameters
sessionId
required
string

Responses

Response samples

Content type
application/json
[
  • {
    }
]

Reports that the session process terminated in the engine

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.

Authorizations:
service-token
path Parameters
sessionId
required
string
Request Body schema: application/json
required
projectId
required
string <uuid>
to
required
string
Enum: "closing" "closed" "closed_abnormally"

closing is NOT terminal: it means "I am releasing this session". If another replica adopts it, it stays alive; if no one adopts it before the timeout, the drain sends closed_abnormally. created and active are left out because entering them is a decision made by the api, never by the engine reporting a termination.

reason
string

The ORIGIN of the termination, not a diagnosis by elimination.

Responses

Request samples

Content type
application/json
{
  • "projectId": "01JC4Z0000PROJETO0000000001",
  • "to": "closed_abnormally",
  • "reason": "heartbeat_timeout"
}

Response samples

Content type
application/json
{
  • "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"
}

llm

Lists what each LLM provider can do, independent of the model

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).

Authorizations:
bearer

Responses

Response samples

Content type
application/json
[
  • {
    }
]

Resolves which model an agent uses, and where it came from

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.

Authorizations:
bearer
path Parameters
projectId
required
string
agentSlug
required
string
Example: dev-api

Responses

Response samples

Content type
application/json
{
  • "modelId": "01JC4Z0000MODELO00000000001",
  • "origin": "project",
  • "routingPreference": null,
  • "skipped": [
    ],
  • "herdadoDoLead": "secops"
}

Pins an agent's model in THIS project

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.

Authorizations:
bearer
path Parameters
projectId
required
string
agentSlug
required
string
Example: dev-api
Request Body schema: application/json
required
modelId
required
string <uuid>

Model to pin to this scope. Must exist in GET /models.

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; null clears it; a value for a model whose provider does not declare the routingPreference capability (GET /llm/provider-capabilities) is refused with 422. It travels WITH this binding: it never cascades on its own.

Responses

Request samples

Content type
application/json
{
  • "modelId": "9b1c2d3e-4f50-4a61-8b72-0c3d4e5f6a7b",
  • "routingPreference": "throughput"
}

Response samples

Content type
application/json
{
  • "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"
}

Makes the agent go back to inheriting the area's model

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.

Authorizations:
bearer
path Parameters
projectId
required
string
agentSlug
required
string
Example: dev-api

Responses

Breaks down the project spend by agent, over the last 30 days

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.

Authorizations:
bearer
path Parameters
projectId
required
string

Responses

Response samples

Content type
application/json
[
  • {
    }
]

Resolves what an area's default model is, and where it came from

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.

Authorizations:
bearer
path Parameters
projectId
required
string
areaKey
required
string
Example: qa

Responses

Response samples

Content type
application/json
{
  • "modelId": "01JC4Z0000MODELO00000000001",
  • "origin": "project",
  • "routingPreference": null,
  • "skipped": [
    ],
  • "herdadoDoLead": "secops"
}

Sets an area's default model

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.

Authorizations:
bearer
path Parameters
projectId
required
string
areaKey
required
string
Example: qa
Request Body schema: application/json
required
modelId
required
string <uuid>

Model to pin to this scope. Must exist in GET /models.

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; null clears it; a value for a model whose provider does not declare the routingPreference capability (GET /llm/provider-capabilities) is refused with 422. It travels WITH this binding: it never cascades on its own.

Responses

Request samples

Content type
application/json
{
  • "modelId": "9b1c2d3e-4f50-4a61-8b72-0c3d4e5f6a7b",
  • "routingPreference": "throughput"
}

Response samples

Content type
application/json
{
  • "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"
}

Makes the area go back to inheriting the project's model

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.

Authorizations:
bearer
path Parameters
projectId
required
string
areaKey
required
string
Example: qa

Responses

Reads the project budget and how much has already been spent

Values in micro-USD. Requires maintainer — it is cost information.

Authorizations:
bearer
path Parameters
projectId
required
string

Responses

Response samples

Content type
application/json
{
  • "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"
}

Sets the project spend cap

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.

Authorizations:
bearer
path Parameters
projectId
required
string
Request Body schema: application/json
required
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"

block refuses the call when it would exceed the cap; allow lets it through and only warns.

Responses

Request samples

Content type
application/json
{
  • "limitUsd": 50,
  • "policy": "block"
}

Response samples

Content type
application/json
{
  • "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"
}

Reads the project model binding

This scope's RAW binding — null if the project only inherits from the workspace.

Authorizations:
bearer
path Parameters
projectId
required
string

Responses

Response samples

Content type
application/json
{
  • "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"
}

Sets the project's model

Authorizations:
bearer
path Parameters
projectId
required
string
Request Body schema: application/json
required
modelId
required
string <uuid>

Model to pin to this scope. Must exist in GET /models.

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; null clears it; a value for a model whose provider does not declare the routingPreference capability (GET /llm/provider-capabilities) is refused with 422. It travels WITH this binding: it never cascades on its own.

Responses

Request samples

Content type
application/json
{
  • "modelId": "9b1c2d3e-4f50-4a61-8b72-0c3d4e5f6a7b",
  • "routingPreference": "throughput"
}

Response samples

Content type
application/json
{
  • "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"
}

Resolves the model of several agents and areas in one read

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.

Authorizations:
bearer
path Parameters
projectId
required
string
query Parameters
agents
string
Example: agents=criativo,po,dev-lead

Comma-separated agent slugs.

areas
string
Example: areas=dev,qa,infra

Comma-separated area keys.

Responses

Response samples

Content type
application/json
{
  • "agents": [
    ],
  • "areas": [
    ]
}

Lists the models active IN THE PROJECT's WORKSPACE

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).

Authorizations:
bearer
path Parameters
projectId
required
string

Responses

Response samples

Content type
application/json
{
  • "local": {
    },
  • "cloud": {
    }
}

Reads the session budget and the accumulated spend

This is what feeds the session screen's live token meter.

Authorizations:
bearer
path Parameters
sessionId
required
string

Responses

Response samples

Content type
application/json
{
  • "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"
}

Sets the session spend cap

Same dollar-to-micro-USD conversion as the project budget.

Authorizations:
bearer
path Parameters
sessionId
required
string
Request Body schema: application/json
required
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"

block refuses the call when it would exceed the cap; allow lets it through and only warns.

Responses

Request samples

Content type
application/json
{
  • "limitUsd": 50,
  • "policy": "block"
}

Response samples

Content type
application/json
{
  • "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"
}

Talks to the session's model, with the response streamed

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.

Authorizations:
bearer
path Parameters
projectId
required
string
sessionId
required
string
Request Body schema: application/json
required
text
required
string

The user's message. The response comes back as an SSE stream.

Responses

Request samples

Content type
application/json
{
  • "text": "Summarize what we have already decided about checkout."
}

Resolves which model the session uses, and where it came from

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.

Authorizations:
bearer
path Parameters
projectId
required
string
sessionId
required
string
query Parameters
agentId
string

Agent currently active in this session (e.g. "po", "arquiteto", "dev-lead").

Responses

Response samples

Content type
application/json
{
  • "modelId": "01JC4Z0000MODELO00000000001",
  • "origin": "project",
  • "routingPreference": null,
  • "skipped": [
    ],
  • "herdadoDoLead": "secops"
}

Pins the model for this session

Beats everything else in the cascade for as long as the session lives.

Authorizations:
bearer
path Parameters
projectId
required
string
sessionId
required
string
Request Body schema: application/json
required
modelId
required
string <uuid>

Model to pin to this scope. Must exist in GET /models.

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; null clears it; a value for a model whose provider does not declare the routingPreference capability (GET /llm/provider-capabilities) is refused with 422. It travels WITH this binding: it never cascades on its own.

Responses

Request samples

Content type
application/json
{
  • "modelId": "9b1c2d3e-4f50-4a61-8b72-0c3d4e5f6a7b",
  • "routingPreference": "throughput"
}

Response samples

Content type
application/json
{
  • "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"
}

Breaks down the session spend by agent

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.

Authorizations:
bearer
path Parameters
sessionId
required
string

Responses

Response samples

Content type
application/json
[
  • {
    }
]

My own consumption in this project, by session and by day

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.

Authorizations:
bearer
path Parameters
projectId
required
string
query Parameters
dias
string

Sliding window. Default 30, max 180.

Responses

Response samples

Content type
application/json
{
  • "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": [
    ],
  • "porDia": [
    ]
}

The project's total spend and cost by agent and area

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.

Authorizations:
bearer
path Parameters
projectId
required
string
query Parameters
dias
string

Sliding window. Default 30, max 180.

Responses

Response samples

Content type
application/json
{
  • "projectId": "5a8591dd-4039-49df-9202-96385ba3eff8",
  • "dias": 30,
  • "totalMicros": 1250000,
  • "assinaturaMicros": 0,
  • "inputTokens": 120000,
  • "outputTokens": 35000,
  • "chamadas": 42,
  • "porAgente": [
    ],
  • "porArea": [
    ]
}

How much the owner's keys spent, by provider and by month

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.

Authorizations:
bearer
path Parameters
workspaceId
required
string
query Parameters
meses
required
string

Responses

Response samples

Content type
application/json
{
  • "workspaceId": "9b1c2d3e-4f50-4a61-8b72-0c3d4e5f6a7b",
  • "ownerId": "9b1c2d3e-4f50-4a61-8b72-0c3d4e5f6a7b",
  • "meses": 6,
  • "totalMicros": 1250000,
  • "porProvider": [
    ]
}

Creates a PENDING pull request — nothing downloads yet

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.

Authorizations:
bearer
path Parameters
workspaceId
required
string
Request Body schema: application/json
required
repoId
required
string

<publisher>/<model> on the Hugging Face Hub.

estimatedSizeBytes
number

Comes from the search when the Hub published the size — never guessed by the client.

Responses

Request samples

Content type
application/json
{
  • "repoId": "meta-llama/Llama-3.1-8B-Instruct-GGUF",
  • "estimatedSizeBytes": 4900000000
}

Response samples

Content type
application/json
{
  • "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"
}

Status of a pull request, for the frontend to poll

Authorizations:
bearer
path Parameters
workspaceId
required
string
id
required
string

Responses

Response samples

Content type
application/json
{
  • "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 second confirmation — actually runs the pull

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.

Authorizations:
bearer
path Parameters
workspaceId
required
string
id
required
string

Responses

Response samples

Content type
application/json
{
  • "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"
}

Lists the LLM providers the workspace owner has a key for

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.

Authorizations:
bearer
path Parameters
workspaceId
required
string

Responses

Response samples

Content type
application/json
{
  • "providers": [
    ]
}

Reads the workspace model binding

This scope's RAW binding. It is the root of the cascade.

Authorizations:
bearer
path Parameters
workspaceId
required
string

Responses

Response samples

Content type
application/json
{
  • "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"
}

Sets the default model for the workspace

Applies to every project that has no binding of its own.

Authorizations:
bearer
path Parameters
workspaceId
required
string
Request Body schema: application/json
required
modelId
required
string <uuid>

Model to pin to this scope. Must exist in GET /models.

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; null clears it; a value for a model whose provider does not declare the routingPreference capability (GET /llm/provider-capabilities) is refused with 422. It travels WITH this binding: it never cascades on its own.

Responses

Request samples

Content type
application/json
{
  • "modelId": "9b1c2d3e-4f50-4a61-8b72-0c3d4e5f6a7b",
  • "routingPreference": "throughput"
}

Response samples

Content type
application/json
{
  • "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"
}

Sets a model's curated billing mark (api | subscription | free)

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.

Authorizations:
bearer
path Parameters
modelId
required
string
Request Body schema: application/json
required
billing
required
string
Enum: "api" "subscription" "free"

Responses

Request samples

Content type
application/json
{
  • "billing": "subscription"
}

Response samples

Content type
application/json
{
  • "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"
}

History of a model's price changes

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.

Authorizations:
bearer
path Parameters
modelId
required
string

Responses

Response samples

Content type
application/json
[
  • {
    }
]

Corrects a model's price

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.

Authorizations:
bearer
path Parameters
modelId
required
string
Request Body schema: application/json
required
inputPricePerMillionMicros
required
number

Price per million INPUT tokens, in micro-USD.

outputPricePerMillionMicros
required
number

Price per million OUTPUT tokens, in micro-USD.

Responses

Request samples

Content type
application/json
{
  • "inputPricePerMillionMicros": 2500000,
  • "outputPricePerMillionMicros": 10000000
}

Response samples

Content type
application/json
{
  • "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"
}

Turns models on or off in the selector (batch)

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.

Authorizations:
bearer
path Parameters
workspaceId
required
string
Request Body schema: application/json
required
modelIds
required
Array of strings <uuid> [ items <uuid > ]

Models to turn on or off. Whole batch or nothing.

isActive
required
boolean

true makes the model appear in the selector and be able to receive a new binding. Does not touch availability, which is what sync observed on the provider.

Responses

Request samples

Content type
application/json
{
  • "modelIds": [
    ],
  • "isActive": true
}

Response samples

Content type
application/json
[
  • {
    }
]

Lists the WHOLE catalog, including inactive and unavailable

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.

Authorizations:
bearer
path Parameters
workspaceId
required
string

Responses

Response samples

Content type
application/json
{
  • "local": {
    },
  • "cloud": {
    }
}

Forces the catalog sync the periodic job does, right now

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.

Authorizations:
bearer

Responses

Response samples

Content type
application/json
{
  • "porProvider": [
    ]
}

Marks what the workspace uses each model for (batch)

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.

Authorizations:
bearer
path Parameters
workspaceId
required
string
Request Body schema: application/json
required
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 code, coding, and código in the same filter.

Responses

Request samples

Content type
application/json
{
  • "modelIds": [
    ],
  • "uses": [
    ]
}

Response samples

Content type
application/json
[
  • {
    }
]

Breaks down the workspace's spend by model, provider, project, actor, and day

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.

Authorizations:
bearer
path Parameters
workspaceId
required
string
query Parameters
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.

Responses

Response samples

Content type
application/json
{
  • "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": [
    ],
  • "porProvider": [
    ],
  • "porProjeto": [
    ],
  • "porSessao": [
    ],
  • "porAtor": [
    ],
  • "porOwner": [
    ],
  • "porAgente": [
    ],
  • "porDia": [
    ]
}

My own consumption in this workspace, summed by project

The non-owner's global page (RN-953). Only the caller's rows, by PROJECT and by day, without provider or credential.

Authorizations:
bearer
path Parameters
workspaceId
required
string
query Parameters
dias
string

Sliding window. Default 30, max 180.

Responses

Response samples

Content type
application/json
{
  • "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": [
    ],
  • "porDia": [
    ]
}

projects

Returns a project by id

Authorizations:
bearer
path Parameters
projectId
required
string

Responses

Response samples

Content type
application/json
{
  • "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"
}

Changes the project's name or slug

Authorizations:
bearer
path Parameters
projectId
required
string
Request Body schema: application/json
required
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 draft to ready (Phase 12c — RN-048). auto (new-project default since RN-659): a complete story is already born ready, with no human step. manual: the PO proposes and YOU decide, in the Backlog tab. Changing the default did not rewrite any existing project. The domain validations (DoD/DoR/RF/rule/modules) are the SAME in both modes; the mode only changes who triggers it.

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).

Responses

Request samples

Content type
application/json
{
  • "name": "Checkout",
  • "slug": "checkout",
  • "maxConsecutiveBlocked": 3,
  • "storyPromotion": "auto",
  • "language": "pt-BR"
}

Response samples

Content type
application/json
{
  • "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"
}

Removes the project

Takes its sessions, event log, and proposed actions along with it.

Authorizations:
bearer
path Parameters
projectId
required
string

Responses

Response samples

Content type
application/json
{
  • "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"
}

State of the project's container (the current image decision)

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.

Authorizations:
bearer
path Parameters
projectId
required
string

Responses

Response samples

Content type
application/json
{
  • "status": "sem_decisao",
  • "decisao": {
    },
  • "version": 0,
  • "eventId": "01JC4Z0000EVENTO000000001",
  • "decidedAt": { }
}

Lifecycle of the project's container (recorded state)

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).

Authorizations:
bearer
path Parameters
projectId
required
string

Responses

Response samples

Content type
application/json
{
  • "status": "provisioning",
  • "imageVersion": 1,
  • "resources": {
    },
  • "failureReason": null,
  • "createdAt": "2019-08-24T14:15:22Z",
  • "statusChangedAt": "2019-08-24T14:15:22Z",
  • "observado": {
    },
  • "naoObservado": "broker-nao-configurado",
  • "detalheDaObservacao": null
}

Declared network of the container (artifact.container_network)

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).

Authorizations:
bearer
path Parameters
projectId
required
string

Responses

Response samples

Content type
application/json
{
  • "ports": [
    ],
  • "dns": [
    ],
  • "extraHosts": [
    ],
  • "version": 1
}

Declares the container network (human only)

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.

Authorizations:
bearer
path Parameters
projectId
required
string
Request Body schema: application/json
required
Array of objects (PortaDto)

Up to 10 TCP ports published on 127.0.0.1 only. host 1024-65535, never an installation port, no duplicates.

dns
Array of strings

Up to 3 literal IPs.

Array of objects (HostExtraDto)

Up to 10 --add-host entries: name in [a-z0-9.-], never localhost or an installation service, IPv4 literal.

Responses

Request samples

Content type
application/json
{
  • "ports": [
    ],
  • "dns": [
    ],
  • "extraHosts": [
    ]
}

Response samples

Content type
application/json
{
  • "ports": [
    ],
  • "dns": [
    ],
  • "extraHosts": [
    ],
  • "version": 1
}

Converts the project's execution mode

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.

Authorizations:
bearer
path Parameters
projectId
required
string
Request Body schema: application/json
required
executionMode
required
string
Enum: "container" "mounted" "runner"

The new execution mode. It may repeat the current project mode — in that case, only workspacePath really changes.

workspacePath
string

The absolute path of the user folder — required when executionMode is mounted or runner, refused when it is container. Same validation as creation (RN-170/RN-422/RN-423): in mounted, it must exist and be writable from inside the container; in runner, only the format is validated now — the real confirmation comes from the runner connecting again.

Responses

Request samples

Content type
application/json
{
  • "executionMode": "runner",
  • "workspacePath": "/home/you/projects/store"
}

Response samples

Content type
application/json
{
  • "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"
}

Lists the project's members

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.

Authorizations:
bearer
path Parameters
projectId
required
string

Responses

Response samples

Content type
application/json
[
  • {
    }
]

Associates a user with the project

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.

Authorizations:
bearer
path Parameters
projectId
required
string
Request Body schema: application/json
required
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 owner, and changing your own role (either way). On a WORKSPACE it is simply the role, with only the second of those caps: you cannot change your own.

Responses

Request samples

Content type
application/json
{
  • "userId": "3f1b2c8e-5a4d-4b7e-9c10-2d6f8a1b4c33",
  • "role": "developer"
}

Response samples

Content type
application/json
{
  • "projectId": "01JC4Z0000PROJETO0000000001",
  • "userId": "01JC4Z0000USUARIO0000000002",
  • "role": "maintainer",
  • "createdAt": "2026-07-22T08:05:00.000Z"
}

Disassociates a user from the project

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.

Authorizations:
bearer
path Parameters
projectId
required
string
userId
required
string

Responses

Declares (or clears) the project's mirror destination

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.

Authorizations:
bearer
path Parameters
projectId
required
string
Request Body schema: application/json
required
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 null — the key is REQUIRED, omitting it is a 400 — to clear the destination and turn the mirror off; null is the normal state of a project, not an error. Only mounted/runner projects can have one: in container the source is a server-side managed volume the local agent cannot see, and the request is refused with 400 naming that reason. The API validates ONLY the LEXICAL shape (absolute, no ../., never the root, a system folder, or overlapping Brabo's own checkout) plus the two directions of the origin↔destination loop (the destination cannot be inside workspacePath, nor contain it) — it never touches the disk, because it cannot see the machine where the destination will live, exactly as in runner mode (RN-423). Resolving symlinks is the local agent’s half of the guard, not this one.

Responses

Request samples

Content type
application/json
{
  • "mirrorPath": "/home/you/mirrors/store"
}

Response samples

Content type
application/json
{
  • "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"
}

What the last mirror round did (RN-517)

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.

Authorizations:
bearer
path Parameters
projectId
required
string

Responses

Response samples

Content type
application/json
{
  • "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
}

Reads the project's permissions.json

It's the physical file at the root of the project's workspace, not a database column.

Authorizations:
bearer
path Parameters
projectId
required
string

Responses

Response samples

Content type
application/json
{
  • "allow": [
    ],
  • "deny": [
    ],
  • "ask": [
    ]
}

Rewrites the project's permissions.json

Replaces all three lists at once. deny still beats allow in evaluation — there's no way to allow something denied through here.

Authorizations:
bearer
path Parameters
projectId
required
string
Request Body schema: application/json
required
allow
required
Array of strings

Automatically approved patterns.

deny
required
Array of strings

Forbidden patterns. deny beats allow and beats the agent's autonomy.

ask
required
Array of strings

Patterns that always ask for a human decision.

Responses

Request samples

Content type
application/json
{
  • "allow": [
    ],
  • "deny": [
    ],
  • "ask": [
    ]
}

Response samples

Content type
application/json
{
  • "allow": [
    ],
  • "deny": [
    ],
  • "ask": [
    ]
}

Lists your own Personal Access Tokens for this project

Never includes the raw token — only what was already issued.

Authorizations:
bearer
path Parameters
projectId
required
string

Responses

Response samples

Content type
application/json
[
  • {
    }
]

Issues a Personal Access Token for the local runner

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.

Authorizations:
bearer
path Parameters
projectId
required
string
Request Body schema: application/json
required
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).

Responses

Request samples

Content type
application/json
{
  • "name": "laptop",
  • "expiresInDays": 90
}

Response samples

Content type
application/json
{
  • "id": "01JC4Z0000PAT000000000001",
  • "name": "laptop",
  • "projectId": "01JC4Z0000PROJETO000000001",
  • "createdAt": "2026-08-22T12:00:00.000Z",
  • "expiresAt": null,
  • "revokedAt": null,
  • "lastUsedAt": null,
  • "token": "brb_9f8a..."
}

Revokes one of your own Personal Access Tokens

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.

Authorizations:
bearer
path Parameters
projectId
required
string
tokenId
required
string

Responses

Revokes the Personal Access Token of ANY user in the project

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.

Authorizations:
bearer
path Parameters
projectId
required
string
tokenId
required
string

Responses

Lists ALL Personal Access Tokens of the project, from any user

The maintainer view for incident response (RN-427) — includes the owner of each token. Never includes the raw token.

Authorizations:
bearer
path Parameters
projectId
required
string

Responses

Response samples

Content type
application/json
[
  • {
    }
]

Lists your own device keys for this project

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.

Authorizations:
bearer
path Parameters
projectId
required
string

Responses

Response samples

Content type
application/json
[
  • {
    }
]

Revokes one of your own device keys

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.

Authorizations:
bearer
path Parameters
projectId
required
string
deviceKeyId
required
string

Responses

Issues a single-use ticket for the local runner to connect

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.

Authorizations:
bearer
path Parameters
projectId
required
string

Responses

Response samples

Content type
application/json
{
  • "ticket": "string",
  • "expiresAt": "2026-08-09T12:00:30.000Z",
  • "engineWsUrl": "ws://localhost:4000/runner"
}

Issues a single-use ticket for the web Terminal tab

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.

Authorizations:
bearer
path Parameters
projectId
required
string

Responses

Response samples

Content type
application/json
{
  • "ticket": "string",
  • "expiresAt": "2026-08-09T12:00:30.000Z",
  • "engineWsUrl": "ws://localhost:4000/runner"
}

Lists the projects in "runner" mode that this local agent serves

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.

Authorizations:
bearer

Responses

Response samples

Content type
application/json
[
  • {
    }
]

Lists every project in the workspace and its container, if any

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.

Authorizations:
bearer
path Parameters
workspaceId
required
string

Responses

Response samples

Content type
application/json
[
  • {
    }
]

actions

Lists the PENDING actions of the whole project, across any session

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).

Authorizations:
bearer
path Parameters
projectId
required
string
query Parameters
status
string
Example: status=pending
actionType
string
Example: actionType=git_merge

Responses

Response samples

Content type
application/json
[
  • {
    }
]

Lists the autonomy granted to each agent

Only the explicitly recorded rules. An agent with no row here falls back to the project's permissions.json default.

Authorizations:
bearer
path Parameters
projectId
required
string

Responses

Response samples

Content type
application/json
[
  • {
    }
]

Sets an agent's autonomy for an action type

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.

Authorizations:
bearer
path Parameters
projectId
required
string
Request Body schema: application/json
required
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 "*" for EVERY action type of this agent — the "auto mode" of the ApprovalCard (RN-153). A SPECIFIC rule always wins over the wildcard: recording terminal: deny with "*": auto_approve already on still denies terminal.

mode
required
string
Enum: "auto_approve" "require_approval" "deny"

This agent's autonomy for this type. Does NOT override permissions.json: a pattern in deny stays blocked no matter how much autonomy the agent has.

Responses

Request samples

Content type
application/json
{
  • "agentId": "dev-api",
  • "actionType": "terminal",
  • "mode": "auto_approve"
}

Paginates the proposed actions in the session

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).

Authorizations:
bearer
path Parameters
projectId
required
string
sessionId
required
string
query Parameters
afterSeq
string
Example: afterSeq=6
limit
string
Example: limit=50
latest
string
Example: latest=true

Fetches the tail of the session; ignores afterSeq.

status
string
Example: status=pending

Only pending is supported.

Responses

Response samples

Content type
application/json
{
  • "items": [
    ],
  • "nextCursor": 7
}

Proposes an action with external effect

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.

Authorizations:
bearer
path Parameters
projectId
required
string
sessionId
required
string
Request Body schema: application/json
required
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: git_push, git_merge, and git_branch_protect require maintainer, the rest developer.

required
object

Who is proposing it.

required
object

Action parameters, with a shape specific to the actionType.

Responses

Request samples

Content type
application/json
{
  • "actionType": "terminal",
  • "actor": {
    },
  • "payload": {
    }
}

Response samples

Content type
application/json
{
  • "id": "01JC4Z8QK3M7YV2N5T9B0PXHRC",
  • "projectId": "01JC4Z0000PROJETO0000000001",
  • "sessionId": "01JC4Z8QK3M7YV2N5T9B0PXHRA",
  • "seq": 7,
  • "actionType": "terminal",
  • "payload": {
    },
  • "status": "pending",
  • "resolvedPolicy": "require_approval",
  • "actor": {
    },
  • "decidedBy": null,
  • "decidedAt": null,
  • "rejectionReason": null,
  • "executionResult": null,
  • "createdAt": "2026-07-27T14:33:10.900Z",
  • "updatedAt": "2026-07-27T14:33:10.900Z"
}

Approves the action, once

Applies only to this action. The decision stays in the event log with the id of who decided.

Authorizations:
bearer
path Parameters
projectId
required
string
sessionId
required
string
actionId
required
string

Responses

Response samples

Content type
application/json
{
  • "id": "01JC4Z8QK3M7YV2N5T9B0PXHRC",
  • "projectId": "01JC4Z0000PROJETO0000000001",
  • "sessionId": "01JC4Z8QK3M7YV2N5T9B0PXHRA",
  • "seq": 7,
  • "actionType": "terminal",
  • "payload": {
    },
  • "status": "pending",
  • "resolvedPolicy": "require_approval",
  • "actor": {
    },
  • "decidedBy": null,
  • "decidedAt": null,
  • "rejectionReason": null,
  • "executionResult": null,
  • "createdAt": "2026-07-27T14:33:10.900Z",
  • "updatedAt": "2026-07-27T14:33:10.900Z"
}

Approves the action and records the pattern in permissions.json

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.

Authorizations:
bearer
path Parameters
projectId
required
string
sessionId
required
string
actionId
required
string

Responses

Response samples

Content type
application/json
{
  • "id": "01JC4Z8QK3M7YV2N5T9B0PXHRC",
  • "projectId": "01JC4Z0000PROJETO0000000001",
  • "sessionId": "01JC4Z8QK3M7YV2N5T9B0PXHRA",
  • "seq": 7,
  • "actionType": "terminal",
  • "payload": {
    },
  • "status": "pending",
  • "resolvedPolicy": "require_approval",
  • "actor": {
    },
  • "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
}

Denies the action

The reason, if given, stays in the event log alongside the decision.

Authorizations:
bearer
path Parameters
projectId
required
string
sessionId
required
string
actionId
required
string
Request Body schema: application/json
required
reason
string

Stays in the event log alongside the denial. Optional, but recommended.

Responses

Request samples

Content type
application/json
{
  • "reason": "The command would delete the working directory."
}

Response samples

Content type
application/json
{
  • "id": "01JC4Z8QK3M7YV2N5T9B0PXHRC",
  • "projectId": "01JC4Z0000PROJETO0000000001",
  • "sessionId": "01JC4Z8QK3M7YV2N5T9B0PXHRA",
  • "seq": 7,
  • "actionType": "terminal",
  • "payload": {
    },
  • "status": "pending",
  • "resolvedPolicy": "require_approval",
  • "actor": {
    },
  • "decidedBy": null,
  • "decidedAt": null,
  • "rejectionReason": null,
  • "executionResult": null,
  • "createdAt": "2026-07-27T14:33:10.900Z",
  • "updatedAt": "2026-07-27T14:33:10.900Z"
}

execution

The project's agent areas, with each lead's cap

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.

Authorizations:
bearer
path Parameters
projectId
required
string

Responses

Response samples

Content type
application/json
[
  • {
    }
]

Sets (or clears) an area's spend cap

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.

Authorizations:
bearer
path Parameters
projectId
required
string
key
required
string
Request Body schema: application/json
required
limitUsd
required
object or null >= 0

Cap in DOLLARS for the area — same dollar-in, micro-USD-out convention as the project/session budgets (BudgetsController). null CLEARS the cap: the area goes back to unlimited, which is also the default for an area that never had one set.

Responses

Request samples

Content type
application/json
{
  • "limitUsd": 20
}

Response samples

Content type
application/json
{
  • "id": "01JC4Z8QK3M7YV2N5T9B0PXHRA",
  • "projectId": "01JC4Z8QK3M7YV2N5T9B0PXHRA",
  • "key": "dev",
  • "leadAgentId": "dev-lead",
  • "maxParallel": 2,
  • "budgetMicros": 20000000,
  • "spentMicros": 4300000,
  • "members": [
    ]
}

Changes an area's parallelism cap

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.

Authorizations:
bearer
path Parameters
projectId
required
string
key
required
string
Request Body schema: application/json
required
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.

Responses

Request samples

Content type
application/json
{
  • "maxParallel": 4
}

Response samples

Content type
application/json
{
  • "id": "01JC4Z8QK3M7YV2N5T9B0PXHRA",
  • "projectId": "01JC4Z8QK3M7YV2N5T9B0PXHRA",
  • "key": "dev",
  • "leadAgentId": "dev-lead",
  • "maxParallel": 2,
  • "budgetMicros": 20000000,
  • "spentMicros": 4300000,
  • "members": [
    ]
}

Activates the execution phase and starts one dev agent per module

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.

Authorizations:
bearer
path Parameters
projectId
required
string
Request Body schema: application/json
required
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 blocked. It is what stops an agent from spending its budget in a correction loop.

devAgentImpl
string
Enum: "real" "noop"

noop starts the dev agents WITHOUT an LLM, to exercise the execution infrastructure end to end without spending tokens. Omitted uses real.

terminalAllowPatterns
Array of strings

Patterns released in permissions.json for the dev agents to run the test suite. deny still wins over anything listed here.

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.

Responses

Request samples

Content type
application/json
{
  • "taskBudgetMicros": 500000,
  • "maxGateCorrections": 3,
  • "devAgentImpl": "real",
  • "terminalAllowPatterns": [
    ],
  • "originSessionId": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
}

Response samples

Content type
application/json
{
  • "sessionId": "01JC4Z8QK3M7YV2N5T9B0PXHRA",
  • "modules": [
    ]
}

Returns the project's current execution session, or nothing

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.

Authorizations:
bearer
path Parameters
projectId
required
string

Responses

Response samples

Content type
application/json
{
  • "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"
}

Rearms a dev agent tripped by the circuit breaker

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.

Authorizations:
bearer
path Parameters
projectId
required
string
sessionId
required
string
agentId
required
string

Responses

Response samples

Content type
application/json
{
  • "ok": true
}

Requests one more dev agent for a module

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.

Authorizations:
bearer
path Parameters
projectId
required
string
sessionId
required
string
Request Body schema: application/json
required
module
required
string

Module name, as it appears in the module_map. Starts a dev agent dedicated to it, in an isolated worktree.

Responses

Request samples

Content type
application/json
{
  • "module": "api"
}

Response samples

Content type
application/json
{
  • "estado": "aguardando_autorizacao",
  • "actionId": "01JC4Z8QK3M7YV2N5T9B0PXHRA",
  • "ativosNaSessao": 2,
  • "maxParallel": 2,
  • "motivo": "string"
}

Unblocks a blocked task

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.

Authorizations:
bearer
path Parameters
projectId
required
string
sessionId
required
string
taskId
required
string

Responses

Response samples

Content type
application/json
{
  • "ok": true
}

anamnesis

Lists an agent's instruction versions

Most recent first, each already carrying its diff against the previous one, computed server-side.

Authorizations:
bearer
path Parameters
projectId
required
string
agent
required
string
Example: dev-api

Responses

Response samples

Content type
application/json
[
  • {
    }
]

Restores a previous version of an agent's instruction

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.

Authorizations:
bearer
path Parameters
projectId
required
string
agent
required
string
Example: dev-api
version
required
string
Example: 2

Version to restore.

Responses

Response samples

Content type
application/json
{
  • "agent": "dev-api",
  • "restoredFrom": 2,
  • "toVersion": 5,
  • "cacheInvalidated": true
}

Runs the Anamnese now, without waiting for the tick

Requires maintainer for the same reason as the Psychologist reanalysis: it runs the ToolLoop and spends real budget.

Authorizations:
bearer
path Parameters
projectId
required
string

Responses

Response samples

Content type
application/json
{
  • "ok": true
}

Returns a project event by id, resolving its session

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.

Authorizations:
bearer
path Parameters
projectId
required
string
eventId
required
string

Responses

Response samples

Content type
application/json
{
  • "id": "01JC4Z8QK3M7YV2N5T9B0PXHRB",
  • "sessionId": "01JC4Z8QK3M7YV2N5T9B0PXHRA",
  • "seq": 41,
  • "type": "chat.message",
  • "actor": {
    },
  • "payload": {
    },
  • "createdAt": "2026-07-27T14:30:58.512Z"
}

Lists the instruction history of every agent in the project

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.

Authorizations:
bearer
path Parameters
projectId
required
string

Responses

Response samples

Content type
application/json
[
  • {
    }
]

Returns the proficiency profile

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.

Authorizations:
bearer
path Parameters
projectId
required
string

Responses

Response samples

Content type
application/json
[
  • {
    }
]

Deletes your own profile and records the opt-out

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.

Authorizations:
bearer
path Parameters
projectId
required
string

Responses

Response samples

Content type
application/json
{
  • "deleted": 2,
  • "optedOut": true
}

Re-enables the user's own profiling

Undoes the opt-out. Profiles start being derived again from the next round on.

Authorizations:
bearer
path Parameters
projectId
required
string

Responses

Response samples

Content type
application/json
{
  • "optedOut": false
}

backlog

Returns the current module_map, the ADRs, and the pending items

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.

Authorizations:
bearer
path Parameters
projectId
required
string

Responses

Response samples

Content type
application/json
{
  • "moduleMap": {
    },
  • "adrs": [],
  • "pendencies": [
    ],
  • "c4Diagram": {
    }
}

Returns the epic → story → task tree

Nested and complete, in a single call. ARCHIVED stories (RN-727) are not listed. Backlog is written by agents, through the /internal/* routes.

Authorizations:
bearer
path Parameters
projectId
required
string

Responses

Response samples

Content type
application/json
[
  • {
    }
]

Computes business-rule → stories coverage

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.

Authorizations:
bearer
path Parameters
projectId
required
string

Responses

Response samples

Content type
application/json
{
  • "rules": [
    ],
  • "uncoveredCount": 2
}

Lists infra PRs and the gate stage of each one

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.

Authorizations:
bearer
path Parameters
projectId
required
string

Responses

Response samples

Content type
application/json
[
  • {
    }
]

Lists the agents' open questions of the project (RN-858)

Status comes from the event log: open is an artifact.open_question without open_question.answered. Newest first, capped at 50 with total.

Authorizations:
bearer
path Parameters
projectId
required
string
query Parameters
status
string
Default: "open"
Enum: "open" "answered" "all"

Responses

Response samples

Content type
application/json
{
  • "itens": [
    ],
  • "total": 0
}

Answers an open question (RN-858)

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.

Authorizations:
bearer
path Parameters
projectId
required
string
questionId
required
string
Request Body schema: application/json
required
resposta
string

Free-text answer. Send this OR aceitarSugestao: true.

aceitarSugestao
boolean

Accept the agent's suggestion as the answer.

Responses

Request samples

Content type
application/json
{
  • "resposta": "404 for a code that does not exist.",
  • "aceitarSugestao": true
}

Response samples

Content type
application/json
{
  • "questionId": "ffa0420e-f70b-4e5b-99a0-434bff860467",
  • "eventId": "string",
  • "sessionId": "string",
  • "pergunta": "string",
  • "sugestao": "string",
  • "afeta": {
    },
  • "autor": "criativo",
  • "criadaEm": "2019-08-24T14:15:22Z",
  • "status": "open",
  • "autorDaSessao": "string",
  • "resposta": {
    }
}

Returns how to run the deliverable on your machine

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.

Authorizations:
bearer
path Parameters
projectId
required
string

Responses

Response samples

Content type
application/json
{
  • "command": "cp .env.example .env && docker compose up --build",
  • "urls": [],
  • "envExample": true,
  • "app": {
    },
  • "qa": { },
  • "version": 1,
  • "eventId": "01JC4Z0000EVENTORUNINSTRUC01",
  • "declaredAt": "2026-10-11T12:00:00.000Z"
}

Archives a draft story (RN-727)

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.

Authorizations:
bearer
path Parameters
projectId
required
string
storyId
required
string
Request Body schema: application/json
required
reason
string

Optional; recorded in backlog.story_archived.

Responses

Request samples

Content type
application/json
{
  • "reason": "Duplicate of \"Register a user\"."
}

Response samples

Content type
application/json
{
  • "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": [
    ],
  • "rnf": [
    ],
  • "businessRuleIds": [
    ],
  • "dod": [
    ],
  • "dor": [
    ],
  • "moduleIds": [
    ],
  • "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"
}

Returns a refused story to the PO, with the reason

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.

Authorizations:
bearer
path Parameters
projectId
required
string
storyId
required
string
Request Body schema: application/json
required
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.

Responses

Request samples

Content type
application/json
{
  • "reason": "The acceptance criteria don't cover the payment refusal case."
}

Response samples

Content type
application/json
{
  • "ok": true
}

Corrects the title of a draft story (RN-727)

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).

Authorizations:
bearer
path Parameters
projectId
required
string
storyId
required
string
Request Body schema: application/json
required
title
required
string

Trimmed; 1 to 200 characters.

Responses

Request samples

Content type
application/json
{
  • "title": "Register a user"
}

Response samples

Content type
application/json
{
  • "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": [
    ],
  • "rnf": [
    ],
  • "businessRuleIds": [
    ],
  • "dod": [
    ],
  • "dor": [
    ],
  • "moduleIds": [
    ],
  • "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"
}

Promotes stories the PO proposed to `ready`

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.

Authorizations:
bearer
path Parameters
projectId
required
string
Request Body schema: application/json
required
storyIds
required
Array of strings

Ids of the stories to promote to ready. A single one is a batch of 1. Each story is validated and promoted on its own — one that fails does not block the others.

Responses

Request samples

Content type
application/json
{
  • "storyIds": [
    ]
}

Response samples

Content type
application/json
{
  • "promoted": [
    ],
  • "failed": [
    ]
}

psychologist

Lists the Psychologist's hypotheses about the agents

Each hypothesis cites the events that support it (evidenceEventIds) — this is what makes it auditable instead of taken on faith.

Authorizations:
bearer
path Parameters
projectId
required
string

Responses

Response samples

Content type
application/json
[
  • {
    }
]

Accepts a hypothesis

Moves proposed → accepted. NOT a proposed_action: the Psychologist is read-only and nothing here has an external effect.

Authorizations:
bearer
path Parameters
projectId
required
string
hypothesisId
required
string

Responses

Response samples

Content type
application/json
{
  • "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": [
    ],
  • "terminationAnalysis": {
    },
  • "status": "proposed",
  • "decidedBy": null,
  • "decidedAt": null,
  • "createdAt": "2026-07-26T18:00:00.000Z",
  • "updatedAt": "2026-07-26T18:00:00.000Z"
}

Dismisses a hypothesis

Moves proposed → dismissed. Same reasoning as accepting.

Authorizations:
bearer
path Parameters
projectId
required
string
hypothesisId
required
string

Responses

Response samples

Content type
application/json
{
  • "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": [
    ],
  • "terminationAnalysis": {
    },
  • "status": "proposed",
  • "decidedBy": null,
  • "decidedAt": null,
  • "createdAt": "2026-07-26T18:00:00.000Z",
  • "updatedAt": "2026-07-26T18:00:00.000Z"
}

Lists the analysis rounds with tier and real cost

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.

Authorizations:
bearer
path Parameters
projectId
required
string

Responses

Response samples

Content type
application/json
[
  • {
    }
]

Reports whether the Psychologist can run a NEW analysis today

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.

Authorizations:
bearer

Responses

Response samples

Content type
application/json
{
  • "enabled": false
}

Triggers a reanalysis of the session

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.

Authorizations:
bearer
path Parameters
projectId
required
string
sessionId
required
string

Responses

Response samples

Content type
application/json
{
  • "ok": true
}

rag

Index coverage: indexed files/sessions against the real total

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.

Authorizations:
bearer
path Parameters
projectId
required
string

Responses

Response samples

Content type
application/json
{
  • "docs": {
    },
  • "adr": {
    },
  • "session": {
    },
  • "local": {
    },
  • "chunksTotal": 0,
  • "chunksWithoutVector": 0
}

Judges one hit of one search as useful or irrelevant (RN-480)

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.

Authorizations:
bearer
path Parameters
projectId
required
string
Request Body schema: application/json
required
searchId
required
string <uuid>

The searchId returned by the search that produced this hit — never invented by the client.

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.

Responses

Request samples

Content type
application/json
{
  • "searchId": "9b1a67f9-9477-48e5-8a6c-11b70245e1d9",
  • "chunkId": "90e1f3df-a8a1-438e-8dd5-10e45398d051",
  • "verdict": "util"
}

Response samples

Content type
application/json
{
  • "searchId": "9b1a67f9-9477-48e5-8a6c-11b70245e1d9",
  • "chunkId": "90e1f3df-a8a1-438e-8dd5-10e45398d051",
  • "verdict": "util",
  • "rank": 0
}

Attaches a local folder (browser upload) as read-only reference (RN-455, ADR 0113)

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.

Authorizations:
bearer
path Parameters
projectId
required
string
Request Body schema: application/json
required
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.

Responses

Request samples

Content type
application/json
{
  • "folderName": "brabo",
  • "files": [
    ]
}

Response samples

Content type
application/json
{
  • "folderName": "string",
  • "filesIndexed": 0,
  • "filesSkipped": 0,
  • "chunksCreated": 0,
  • "embedding": {
    }
}

Reindexes the project's docs/ADR/sessions (idempotent full rebuild)

"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.

Authorizations:
bearer
path Parameters
projectId
required
string

Responses

Response samples

Content type
application/json
{
  • "docs": {
    },
  • "sessions": {
    },
  • "embeddingAvailable": true,
  • "embeddingReason": "string"
}

Hybrid search (vector + lexical) over the project's indexed chunks

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.

Authorizations:
bearer
path Parameters
projectId
required
string
Request Body schema: application/json
required
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.

Responses

Request samples

Content type
application/json
{
  • "query": "how does the PR gate work",
  • "scopes": [
    ],
  • "limit": 10
}

Response samples

Content type
application/json
{
  • "query": "string",
  • "searchId": "9b1a67f9-9477-48e5-8a6c-11b70245e1d9",
  • "hits": [
    ],
  • "vectorAvailable": true,
  • "vectorUnavailableReason": "string"
}

sessions

Lists the project's sessions

Each item carries technical: true only for the session opened by repository provisioning (linked from repo_bootstraps).

Authorizations:
bearer
path Parameters
projectId
required
string

Responses

Response samples

Content type
application/json
[
  • {
    }
]

Opens a session in the project

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.

Authorizations:
bearer
path Parameters
projectId
required
string
Request Body schema: application/json
required
kind
required
string
Enum: "consultiva" "criativa"

The session's intent. consultiva (consultative) is conversation only; criativa (creative) is the one that produces — it opens ideation with the Creative agent and is the only one that enters execution. REQUIRED: without it the response is 400, because the type is the opener's choice and gets recorded.

name
string <= 80 characters

Friendly name, optional. Does not replace the id's hashtag — screens compose the two. Blank counts as absent.

Responses

Request samples

Content type
application/json
{
  • "kind": "criativa",
  • "name": "Cart checkout"
}

Response samples

Content type
application/json
{
  • "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"
}

Returns a session by id

Authorizations:
bearer
path Parameters
projectId
required
string
sessionId
required
string

Responses

Response samples

Content type
application/json
{
  • "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"
}

Sets or clears the session's friendly name

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.

Authorizations:
bearer
path Parameters
projectId
required
string
sessionId
required
string
Request Body schema: application/json
required
name
required
object or null <= 80 characters

The new name. null (or a blank string) CLEARS the name and the session goes back to identifying itself only by the hashtag — this is the undo path, which is why the field accepts null instead of requiring a removal route.

Responses

Request samples

Content type
application/json
{
  • "name": "Cart checkout"
}

Response samples

Content type
application/json
{
  • "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"
}

Resumes the turn an engine restart interrupted

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.

Authorizations:
bearer
path Parameters
projectId
required
string
sessionId
required
string
agent
required
string

Responses

Response samples

Content type
application/json
{
  • "ok": true
}

Counts the whole session for the context panel

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.

Authorizations:
bearer
path Parameters
projectId
required
string
sessionId
required
string

Responses

Response samples

Content type
application/json
{
  • "sessoesLidas": [
    ],
  • "regras": 11,
  • "decisoes": 6,
  • "itensDeRegras": [
    ],
  • "itensDeDecisoes": [
    ],
  • "artefatosPorAgente": [
    ],
  • "arquivosTocados": {
    }
}

Paginates the session's event log

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).

Authorizations:
bearer
path Parameters
projectId
required
string
sessionId
required
string
query Parameters
afterSeq
string
Example: afterSeq=40

Returns the events with seq greater than this.

limit
string
Example: limit=50
latest
string
Example: latest=true

Fetches the tail of the log; ignores afterSeq.

actionId
string
Example: actionId=01JC4Z0000ACAO00000000000001

Only the events whose payload.actionId is this action — e.g. its proposed_action.created, which carries the policy reason and scopeRoot the action row does not keep (RN-614). Combines with the other parameters; seq is no longer contiguous in the page, so a caller filtering by action must not derive omitted counts from it. An empty value is a 400.

Responses

Response samples

Content type
application/json
{
  • "items": [
    ],
  • "nextCursor": 41
}

Appends an event to the session's log

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.

Authorizations:
bearer
path Parameters
projectId
required
string
sessionId
required
string
Request Body schema: application/json
required
type
required
string

Event type. The full inventory is in docs/reference/events.md.

required
object (ActorDto)
required
object

Free-form shape, specific to each type.

Responses

Request samples

Content type
application/json
{
  • "type": "chat.message",
  • "actor": {
    },
  • "payload": {
    }
}

Response samples

Content type
application/json
{
  • "id": "01JC4Z8QK3M7YV2N5T9B0PXHRB",
  • "sessionId": "01JC4Z8QK3M7YV2N5T9B0PXHRA",
  • "seq": 41,
  • "type": "chat.message",
  • "actor": {
    },
  • "payload": {
    },
  • "createdAt": "2026-07-27T14:30:58.512Z"
}

Returns a log event by id

Exists so the Psychologist's hypothesis evidence can reach the cited event regardless of which page it fell on or the feed's filters.

Authorizations:
bearer
path Parameters
projectId
required
string
sessionId
required
string
eventId
required
string

Responses

Response samples

Content type
application/json
{
  • "id": "01JC4Z8QK3M7YV2N5T9B0PXHRB",
  • "sessionId": "01JC4Z8QK3M7YV2N5T9B0PXHRA",
  • "seq": 41,
  • "type": "chat.message",
  • "actor": {
    },
  • "payload": {
    },
  • "createdAt": "2026-07-27T14:30:58.512Z"
}

Reopens a closed session, keeping everything it had

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).

Authorizations:
bearer
path Parameters
projectId
required
string
sessionId
required
string

Responses

Response samples

Content type
application/json
{
  • "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 language agents answer YOU in, in this session

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).

Authorizations:
bearer
path Parameters
projectId
required
string
sessionId
required
string

Responses

Response samples

Content type
application/json
{
  • "language": "pt-BR",
  • "origin": "interface",
  • "sessionOverride": null,
  • "account": "automatico",
  • "detected": null,
  • "interfaceLocale": "pt-BR",
  • "detectionQuestion": "es"
}

Fixes (or releases) YOUR response language in this session

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.

Authorizations:
bearer
path Parameters
projectId
required
string
sessionId
required
string
Request Body schema: application/json
required
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 null (or automatico) — the key is REQUIRED — to go back to inheriting from your Account.

Responses

Request samples

Content type
application/json
{
  • "language": "en"
}

Response samples

Content type
application/json
{
  • "language": "pt-BR",
  • "origin": "interface",
  • "sessionOverride": null,
  • "account": "automatico",
  • "detected": null,
  • "interfaceLocale": "pt-BR",
  • "detectionQuestion": "es"
}

Issues an opaque, single-use ticket for the session's socket

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.

Authorizations:
bearer
path Parameters
projectId
required
string
sessionId
required
string
Request Body schema: application/json
required
scope
required
string
Enum: "heartbeat" "terminal"

heartbeat requires the viewer role; terminal requires developer — the same minimum role as MIN_ROLE_FOR_ACTION_TYPE.terminal.

Responses

Request samples

Content type
application/json
{
  • "scope": "heartbeat"
}

Response samples

Content type
application/json
{
  • "ticket": "string",
  • "expiresAt": "2026-08-09T12:00:30.000Z"
}

Moves the session to another state

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.

Authorizations:
bearer
path Parameters
projectId
required
string
sessionId
required
string
Request Body schema: application/json
required
status
required
string
Enum: "created" "active" "closing" "closed" "closed_abnormally"

Target state. The state machine refuses an invalid jump with 409 — from created you can only go to active, and there is no going back from a terminal state.

Responses

Request samples

Content type
application/json
{
  • "status": "closing"
}

Response samples

Content type
application/json
{
  • "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"
}

agents

Cancels the active agent's turn in progress

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.

Authorizations:
bearer
path Parameters
projectId
required
string
sessionId
required
string
agent
required
string
Example: po

Slug of the active agent.

Responses

Response samples

Content type
application/json
{
  • "ok": true
}

Sends a message to the active agent

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).

Authorizations:
bearer
path Parameters
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).

Request Body schema: application/json
required
text
required
string [ 1 .. 10000 ] characters

User's message to the session's active agent.

Responses

Request samples

Content type
application/json
{
  • "text": "I want to allow discount coupons at checkout."
}

Response samples

Content type
application/json
{
  • "ok": true,
  • "mensagemId": "01JC4Z0000EVENTO000000000001",
  • "entrega": "enfileirada",
  • "posicao": 2
}

Cancels one message waiting in the agent's queue

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.

Authorizations:
bearer
path Parameters
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 chat.message event (the mensagemId the message route returned).

Responses

Response samples

Content type
application/json
{
  • "ok": true
}

Starts an agent in the session

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.

Authorizations:
bearer
path Parameters
projectId
required
string
sessionId
required
string
agent
required
string
Example: criativo

Agent slug.

Responses

Response samples

Content type
application/json
{
  • "agent": "po",
  • "status": "active"
}

Answers a set of the agent's structured questions

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.

Authorizations:
bearer
path Parameters
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 chat.structured_question event.

Request Body schema: application/json
required
required
object

Answer keyed by question id — the same id that came in chat.structured_question.

property name*
additional property
string

Responses

Request samples

Content type
application/json
{
  • "answers": {
    }
}

Response samples

Content type
application/json
{
  • "ok": true
}

Confirms the architecture is ready and offers the handoff to Infra

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.

Authorizations:
bearer
path Parameters
projectId
required
string
sessionId
required
string

Responses

Response samples

Content type
application/json
{
  • "ok": true,
  • "desfecho": "confirmado",
  • "jaAtendidos": [
    ]
}

Confirms the Criativo's business need has been validated

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.

Authorizations:
bearer
path Parameters
projectId
required
string
sessionId
required
string

Responses

Response samples

Content type
application/json
{
  • "ok": true
}

Lists the session's agent-to-agent handoffs

Authorizations:
bearer
path Parameters
projectId
required
string
sessionId
required
string

Responses

Response samples

Content type
application/json
[
  • {
    }
]

Manually offers a handoff to a chosen agent

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).

Authorizations:
bearer
path Parameters
projectId
required
string
sessionId
required
string
Request Body schema: application/json
required
toAgent
required
string

Slug of the target agent. Must be a lead or an area-less agent — never a subagent.

Responses

Request samples

Content type
application/json
{
  • "toAgent": "staff"
}

Response samples

Content type
application/json
{
  • "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"
}

Accepts a handoff and activates the destination agent

One thing only: transition to accepted AND start the destination agent. It is the normal activation path for every agent other than the Criativo.

Authorizations:
bearer
path Parameters
projectId
required
string
sessionId
required
string
handoffId
required
string

Responses

Response samples

Content type
application/json
{
  • "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"
}

Confirms that the discovery session with the Criativo is done

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.

Authorizations:
bearer
path Parameters
projectId
required
string
sessionId
required
string

Responses

Response samples

Content type
application/json
{
  • "ok": true
}

credentials

Stores an LLM API key imported by the local runner

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.

Authorizations:
bearer
Request Body schema: application/json
required
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 true to replace an existing credential of this provider.

Responses

Request samples

Content type
application/json
{
  • "provider": "anthropic",
  • "apiKey": "string",
  • "origem": "codex: ~/.codex/auth.json",
  • "substituir": true
}

Response samples

Content type
application/json
{
  • "provider": "anthropic",
  • "substituida": true
}

Lists the current user's own credentials

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.

Authorizations:
bearer

Responses

Response samples

Content type
application/json
[
  • {
    }
]

Registers or replaces a provider's API key

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.

Authorizations:
bearer
Request Body schema: application/json
required
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 POST /users/me/credentials/{provider}/test.

Responses

Request samples

Content type
application/json
{
  • "provider": "anthropic",
  • "apiKey": "sk-ant-api03-…"
}

Response samples

Content type
application/json
{
  • "id": "01JC4Z0000CREDENCIAL00000001",
  • "provider": "anthropic",
  • "createdAt": "2026-07-10T10:00:00.000Z",
  • "updatedAt": "2026-07-10T10:00:00.000Z"
}

Removes a provider's credential

Idempotent: deleting what does not exist also responds { ok: true }.

Authorizations:
bearer
path Parameters
provider
required
string
Example: anthropic

Responses

Response samples

Content type
application/json
{
  • "ok": true
}

Checks the ALREADY stored credential against the provider

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.

Authorizations:
bearer
path Parameters
provider
required
string
Example: openrouter

Responses

Response samples

Content type
application/json
{
  • "resultado": "recusado",
  • "motivo": "connection test failed for openrouter: openrouter responded 401"
}

Registers a git token (encrypted, without testing)

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.

Authorizations:
bearer
Request Body schema: application/json
required
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 POST /users/me/credentials/{provider}/test. The 512-character cap is protection against an absurd payload, not format validation.

Responses

Request samples

Content type
application/json
{
  • "provider": "github",
  • "token": "ghp_…"
}

Response samples

Content type
application/json
{
  • "id": "01JC4Z0000CREDENCIAL00000001",
  • "provider": "anthropic",
  • "createdAt": "2026-07-10T10:00:00.000Z",
  • "updatedAt": "2026-07-10T10:00:00.000Z"
}

users

Lists the authenticated user's own MACHINE device keys

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.

Authorizations:
bearer

Responses

Response samples

Content type
application/json
[
  • {
    }
]

Revokes one of the authenticated user’s own MACHINE device keys

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.

Authorizations:
bearer
path Parameters
deviceKeyId
required
string

Responses

Reads the authenticated user's interface language and agent response language

Authorizations:
bearer

Responses

Response samples

Content type
application/json
{
  • "locale": "pt-BR",
  • "responseLanguage": "automatico",
  • "detectedLanguage": null,
  • "detectedLanguageConfirmedAt": "2019-08-24T14:15:22Z",
  • "effectiveResponseLanguage": {
    }
}

Writes the authenticated user's interface language and/or agent response language

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.

Authorizations:
bearer
Request Body schema: application/json
required
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): automatico or ANY BCP-47 code whose language the server recognizes — an open list, unlike locale. Stored canonical (pt-br becomes pt-BR). An unrecognized code is a 400 that names the rule. Never changes locale.

Responses

Request samples

Content type
application/json
{
  • "locale": "en",
  • "responseLanguage": "es"
}

Response samples

Content type
application/json
{
  • "locale": "pt-BR",
  • "responseLanguage": "automatico",
  • "detectedLanguage": null,
  • "detectedLanguageConfirmedAt": "2019-08-24T14:15:22Z",
  • "effectiveResponseLanguage": {
    }
}

Answers the detected-language question

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.

Authorizations:
bearer
Request Body schema: application/json
required
language
required
string <= 35 characters

The language the question was about — the detectionQuestion of GET .../sessions/:sessionId/response-language.

answer
required
string
Enum: "confirm" "decline"

confirm stores it as your detected-and-confirmed language (it then answers you wherever your Account is on automatic); 409 deteccao_mudou when your messages no longer point to it. decline stores the refusal and this language is never asked again (confirming it later clears the refusal).

Responses

Request samples

Content type
application/json
{
  • "language": "es",
  • "answer": "confirm"
}

Response samples

Content type
application/json
{
  • "locale": "pt-BR",
  • "responseLanguage": "automatico",
  • "detectedLanguage": null,
  • "detectedLanguageConfirmedAt": "2019-08-24T14:15:22Z",
  • "effectiveResponseLanguage": {
    }
}

workspaces

Lists the caller's workspaces

Already filtered by association — there is no global listing. Each item carries the caller's own role.

Authorizations:
bearer

Responses

Response samples

Content type
application/json
[
  • {
    }
]

Creates a workspace

Requires no role: whoever creates it becomes owner. It's the only entry point for someone who doesn't belong to any workspace yet.

Authorizations:
bearer
Request Body schema: application/json
required
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.

Responses

Request samples

Content type
application/json
{
  • "name": "Acme Corp",
  • "slug": "acme-corp"
}

Response samples

Content type
application/json
{
  • "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"
}

Returns a workspace by id

Authorizations:
bearer
path Parameters
workspaceId
required
string

Responses

Response samples

Content type
application/json
{
  • "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"
}

Changes the workspace's name or slug

Authorizations:
bearer
path Parameters
workspaceId
required
string
Request Body schema: application/json
required
name
string >= 2 characters
slug
string^[a-z0-9]+(-[a-z0-9]+)*$

kebab-case. Unique in the system and visible in the URL.

Responses

Request samples

Content type
application/json
{
  • "name": "Acme Corp",
  • "slug": "acme-corp"
}

Response samples

Content type
application/json
{
  • "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"
}

Removes the workspace

Requires owner. Takes its projects, sessions, and their history along with it.

Authorizations:
bearer
path Parameters
workspaceId
required
string

Responses

Response samples

Content type
application/json
{
  • "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"
}

Lists the workspace's members

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.

Authorizations:
bearer
path Parameters
workspaceId
required
string

Responses

Response samples

Content type
application/json
[
  • {
    }
]

Associates a user with the workspace

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.

Authorizations:
bearer
path Parameters
workspaceId
required
string
Request Body schema: application/json
required
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 owner, and changing your own role (either way). On a WORKSPACE it is simply the role, with only the second of those caps: you cannot change your own.

Responses

Request samples

Content type
application/json
{
  • "userId": "3f1b2c8e-5a4d-4b7e-9c10-2d6f8a1b4c33",
  • "role": "developer"
}

Response samples

Content type
application/json
{
  • "workspaceId": "01JC4Z0000WORKSPACE00000001",
  • "userId": "01JC4Z0000USUARIO0000000002",
  • "role": "maintainer",
  • "createdAt": "2026-07-22T08:00:00.000Z"
}

Disassociates a user from the workspace

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.

Authorizations:
bearer
path Parameters
workspaceId
required
string
userId
required
string

Responses

Transfers the workspace's owner of record

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.

Authorizations:
bearer
path Parameters
workspaceId
required
string
Request Body schema: application/json
required
userId
required
string <uuid>

Id of the user who becomes the owner of record. Must already be an owner of this workspace.

Responses

Request samples

Content type
application/json
{
  • "userId": "3f1b2c8e-5a4d-4b7e-9c10-2d6f8a1b4c33"
}

Response samples

Content type
application/json
{
  • "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"
}

Lists the subfolders of a folder inside the projects base

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.

Authorizations:
bearer
query Parameters
path
string

Absolute path to list. Omitted means the base itself. Must be inside BRABO_PROJECTS_BASE; .., . and relative paths are refused outright rather than resolved.

Responses

Response samples

Content type
application/json
{
  • "base": "/home/voce/brabo",
  • "path": "/home/voce/brabo/clientes",
  • "entries": [
    ],
  • "truncado": false,
  • "arquivos": 12,
  • "simbolicos": 1
}

Lists the workspace's projects

Authorizations:
bearer
path Parameters
workspaceId
required
string

Responses

Response samples

Content type
application/json
[
  • {
    }
]

Creates a project inside the workspace

Authorizations:
bearer
path Parameters
workspaceId
required
string
Request Body schema: application/json
required
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 draft to ready (Phase 12c — RN-048). auto (new-project default since RN-659): a complete story is already born ready, with no human step. manual: the PO proposes and YOU decide, in the Backlog tab. Changing the default did not rewrite any existing project. The domain validations (DoD/DoR/RF/rule/modules) are the SAME in both modes; the mode only changes who triggers it.

executionMode
string
Enum: "container" "mounted" "runner"

WHERE this project's command EXECUTES (RN-169/RN-421 — ADR 0072/0104). container (default): the folder managed by the product inside PROJECT_WORKSPACES_ROOT, the usual behavior. mounted: a folder of YOURS, given in workspacePath, inside the installation-wide base BRABO_PROJECTS_BASE (ADR 0141) — creation validates only the path FORMAT plus that base, and the folder itself is created later, when Infra starts the container (RN-501, ADR 0142). runner: a folder of YOURS that does NOT need a bind-mount — creation only validates the path format and the project is born "unverified"; run brabo-runner --project <id> --dir <folder> on your machine to confirm it (RN-423).

workspacePath
string

The folder's ABSOLUTE path, required when executionMode is mounted or runner, and refused when it is container. In BOTH of them only the FORMAT is validated now — absolute, no .., not the system root, not a system folder, not overlapping the Brabo checkout (RN-422/RN-423). mounted adds one rule: it must sit inside BRABO_PROJECTS_BASE, the single folder of your machine the Brabo containers can see (ADR 0141) — with no base configured the mode is not available on this installation and creation says so. Disk is touched later: by the runner in runner, and when Infra starts the container in mounted (RN-501, ADR 0142).

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).

Responses

Request samples

Content type
application/json
{
  • "name": "Checkout",
  • "slug": "checkout",
  • "maxConsecutiveBlocked": 3,
  • "storyPromotion": "auto",
  • "executionMode": "container",
  • "workspacePath": "/home/you/projects/store",
  • "language": "pt-BR"
}

Response samples

Content type
application/json
{
  • "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 base folder for projects in Mounted mode

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).

Authorizations:
bearer

Responses

Response samples

Content type
application/json
{
  • "projectsBase": "/home/voce/brabo",
  • "brokerConfigurado": false
}

Count of blocked tasks per project

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.

Authorizations:
bearer
path Parameters
workspaceId
required
string

Responses

Response samples

Content type
application/json
[
  • {
    }
]

Everything the dashboard cards render, in one call

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.

Authorizations:
bearer
path Parameters
workspaceId
required
string

Responses

Response samples

Content type
application/json
[
  • {
    }
]

Aggregated workspace summary

Project count, agents that spent tokens this month, and this month's spend — feeds the summary line of the project dashboard.

Authorizations:
bearer
path Parameters
workspaceId
required
string

Responses

Response samples

Content type
application/json
{
  • "activeProjects": 4,
  • "agentCount": 6,
  • "spentMicros": 12500000
}

Turns the Jev tool routing on or off for the workspace

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.

Authorizations:
bearer
path Parameters
workspaceId
required
string
Request Body schema: application/json
required
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.

Responses

Request samples

Content type
application/json
{
  • "enabled": false
}

Response samples

Content type
application/json
{
  • "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"
}

Unread events across multiple projects, in one call

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.

Authorizations:
bearer
path Parameters
workspaceId
required
string
Request Body schema: application/json
required
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)
projectId
required
string <uuid>
afterSeq
required
number >= 0

Last seq THIS browser has already seen in this project. Returns events with strictly greater seq. 0 means "never saw anything" and brings the start of the most recent session.

Responses

Request samples

Content type
application/json
{
  • "cursors": [
    ]
}

Response samples

Content type
application/json
[
  • {
    }
]