openapi: 3.1.0
info:
  title: T25 HTTP API
  version: v1
  description: |
    Control plane da T25. O cockpit, a CLI, o MCP e um script headless usam
    este contrato. Sem SDK obrigatório.

    **Auth:** chave `X-T25-Key-Id` (alias `X-Factory-Key-Id` na janela de
    migração) ou cookie de sessão `factory_session` após OAuth.

    **MCP:** o servidor `npm run mcp` expõe 52 tools em camadas cumulativas:
    21 `read`, 30 `operate`, 38 `approve` e 52 `admin`. Elas chamam este
    contrato HTTP com nomes de operação próprios.
  contact:
    email: convite@t25.io
servers:
  - url: http://localhost:4173
    description: Dev local (`npm run dev`)
tags:
  - name: Authentication
    description: OAuth GitHub/OIDC e logout. Rotas públicas (exceto logout).
  - name: Health
    description: Liveness e readiness do control plane.
  - name: Tasks
    description: Ordens da fábrica — criar, gate, cancel, retry.
  - name: Briefs
    description: Business brief pré-fábrica (draft → in_review → approved).
  - name: Policies
    description: Overlay de política e config (não reescreve t25.yaml).
  - name: Events
    description: Stream SSE do chão de fábrica.
  - name: Agents
    description: Perfis de CLI por papel.
security:
  - ApiKey: []
  - Session: []

components:
  securitySchemes:
    ApiKey:
      type: apiKey
      in: header
      name: X-T25-Key-Id
      description: ID da chave. Segredo no header Authorization Bearer se o servidor exigir.
    Session:
      type: apiKey
      in: cookie
      name: factory_session
  schemas:
    Task:
      type: object
      properties:
        id: { type: string, example: TASK-0042 }
        title: { type: string }
        state:
          type: string
          enum: [RECEIVED, SPEC, PLAN, AWAITING_APPROVAL, IMPLEMENTING, QA, REVIEW, PR_OPEN, DOCS, DONE, NEEDS_INPUT, FAILED, CANCELLED]
        risk: { type: string, enum: [low, medium, high] }
        projectId: { type: string, example: default }
    Brief:
      type: object
      properties:
        id: { type: string }
        title: { type: string }
        problem: { type: string }
        status: { type: string, enum: [draft, in_review, approved, rejected] }
        intakeTaskId: { type: string, nullable: true }

paths:
  /api/auth/config:
    get:
      tags: [Authentication]
      operationId: getAuthConfig
      summary: Fluxos OAuth habilitados
      security: []
      responses:
        '200':
          description: Quais IdPs estão ligados
          content:
            application/json:
              example:
                oauth: true
                github: true
                oidc: false
                google: false

  /api/auth/github:
    get:
      tags: [Authentication]
      operationId: githubOAuthStart
      summary: Inicia OAuth GitHub (PKCE)
      security: []
      responses:
        '302':
          description: Redirect para github.com/login/oauth/authorize

  /api/auth/logout:
    post:
      tags: [Authentication]
      operationId: logout
      summary: Encerra a sessão
      responses:
        '204':
          description: Cookie removido

  /api/v1/health:
    get:
      tags: [Health]
      operationId: getHealth
      summary: Liveness + adapters + fila
      responses:
        '200':
          description: Processo no ar
          content:
            application/json:
              example:
                ok: true
                adapters:
                  claude: { available: true }
                  codex: { available: true }
                queue: { pending: 0, inFlight: 0 }
                role: operator
                capabilities: { operate: true, approve: false, admin: false }
                version: v1

  /api/v1/readiness:
    get:
      tags: [Health]
      operationId: getReadiness
      summary: Control plane (Postgres) acessível
      responses:
        '200':
          content:
            application/json:
              example: { ok: true }
        '503':
          description: control plane indisponível

  /api/v1/agents:
    get:
      tags: [Agents]
      operationId: listAgents
      summary: Papéis e CLIs configurados
      responses:
        '200':
          content:
            application/json:
              example:
                - { id: triage, name: Triage, providers: [claude, codex] }
                - { id: planner, name: Planner, providers: [codex] }

  /api/v1/agents/{role}:
    put:
      tags: [Agents]
      operationId: putAgentRole
      summary: Atualiza a cadeia de CLIs de um papel (admin)
      parameters:
        - in: path
          name: role
          required: true
          schema: { type: string, example: planner }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                providers:
                  type: array
                  items: { type: string }
            example:
              providers: [codex, claude]
      responses:
        '200':
          description: Papel atualizado
        '400':
          description: Papel ou providers inválidos

  /api/v1/tasks:
    get:
      tags: [Tasks]
      operationId: listTasks
      summary: Lista ordens visíveis
      responses:
        '200':
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/Task' }
              example:
                - { id: TASK-0042, title: Endpoint de métricas, state: AWAITING_APPROVAL, risk: medium, projectId: default }
    post:
      tags: [Tasks]
      operationId: createTask
      summary: Abre uma ordem
      description: |
        Capability `operate`. Header `Idempotency-Key` devolve a mesma task se a
        chave já foi usada.
      parameters:
        - in: header
          name: Idempotency-Key
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [title]
              properties:
                title: { type: string }
                body: { type: string }
                type: { type: string, example: feature }
                risk: { type: string, enum: [low, medium, high] }
                projectId: { type: string, example: default }
                budgetUsd: { type: number }
            example:
              title: Adicionar endpoint de métricas
              body: Expor latência por rota
              type: feature
              risk: medium
              projectId: default
      responses:
        '201':
          description: Task criada
          content:
            application/json:
              example:
                id: TASK-0042
                title: Adicionar endpoint de métricas
                state: RECEIVED
                risk: medium
                projectId: default

  /api/v1/tasks/{id}:
    get:
      tags: [Tasks]
      operationId: getTask
      summary: Detalhe da ordem + runs
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string, example: TASK-0042 }
      responses:
        '200':
          content:
            application/json:
              example:
                task: { id: TASK-0042, title: Endpoint de métricas, state: SPEC }
                runs: []
        '404':
          description: Task não encontrada

  /api/v1/tasks/{id}/approve:
    post:
      tags: [Tasks]
      operationId: approveTask
      summary: Gate de plano (go/no-go)
      description: Capability `approve`. Não aprova briefing e não faz merge.
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Task avançou

  /api/v1/tasks/{id}/answer:
    post:
      tags: [Tasks]
      operationId: answerTask
      summary: Responde NEEDS_INPUT
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [answers]
              properties:
                answers: { type: string }
            example:
              answers: Usa o adapter Codex neste papel.
      responses:
        '200':
          description: Task saiu de NEEDS_INPUT

  /api/v1/tasks/{id}/cancel:
    post:
      tags: [Tasks]
      operationId: cancelTask
      summary: Cancela a ordem
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Estado CANCELLED

  /api/v1/tasks/{id}/retry:
    post:
      tags: [Tasks]
      operationId: retryTask
      summary: Retry de FAILED
      description: Respeita max_retries e backoff. 400 se ainda não pode.
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Reenfileirada
        '400':
          description: retry indisponível até …

  /api/v1/briefs:
    get:
      tags: [Briefs]
      operationId: listBriefs
      summary: Lista briefs
      parameters:
        - in: query
          name: status
          schema: { type: string, enum: [draft, in_review, approved, rejected] }
        - in: query
          name: projectId
          schema: { type: string }
      responses:
        '200':
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/Brief' }
    post:
      tags: [Briefs]
      operationId: createBrief
      summary: Cria rascunho
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [title, problem]
              properties:
                title: { type: string }
                problem: { type: string }
                audience: { type: string }
            example:
              title: Checkout em uma etapa
              problem: O funil perde gente no endereço
      responses:
        '201':
          description: Brief em draft

  /api/v1/briefs/{id}/intake:
    post:
      tags: [Briefs]
      operationId: intakeBrief
      summary: Brief approved → Task RECEIVED
      description: Só se status=approved. Idempotente se intakeTaskId já existe.
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Brief com intakeTaskId
        '409':
          description: Brief ainda não aprovado

  /api/v1/policies/{projectId}:
    get:
      tags: [Policies]
      operationId: getPolicy
      summary: Política efetiva do projeto
      parameters:
        - in: path
          name: projectId
          required: true
          schema: { type: string, example: default }
      responses:
        '200':
          content:
            application/json:
              example:
                projectId: default
                budgetUsd: 25
                requirePlanApproval: true
    put:
      tags: [Policies]
      operationId: putPolicy
      summary: Overlay de política (admin)
      parameters:
        - in: path
          name: projectId
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            example:
              budgetUsd: 40
              requirePlanApproval: true
      responses:
        '200':
          description: Overlay gravado

  /api/v1/config:
    get:
      tags: [Policies]
      operationId: getConfig
      summary: Config sanitizada (sem segredos)
      responses:
        '200':
          description: t25.yaml efetivo + overlay
    put:
      tags: [Policies]
      operationId: putConfig
      summary: Overlay de config (admin)
      requestBody:
        content:
          application/json:
            example:
              limits: { max_files_changed: 20 }
      responses:
        '200':
          description: Overlay gravado

  /api/v1/events:
    get:
      tags: [Events]
      operationId: streamEvents
      summary: SSE do chão de fábrica
      description: |
        `Accept: text/event-stream`. Eventos `task.state_changed`, run log, etc.
      responses:
        '200':
          description: text/event-stream
          content:
            text/event-stream:
              example: |
                event: task.state_changed
                data: {"taskId":"TASK-0042","state":"AWAITING_APPROVAL"}
