openapi: 3.1.0
info:
  title: T25 HTTP API
  version: v1
  description: |
    Control plane for T25. The cockpit, CLI, MCP and a headless script use
    this contract. No SDK required.

    **Auth:** key `X-T25-Key-Id` (alias `X-Factory-Key-Id` during the
    migration window) or the `factory_session` session cookie after OAuth.

    **MCP:** `npm run mcp` exposes 52 tools in cumulative layers:
    21 `read`, 30 `operate`, 38 `approve` and 52 `admin`. They call this
    HTTP contract under their own operation names.
  contact:
    email: convite@t25.io
servers:
  - url: http://localhost:4173
    description: Local development (`npm run dev`)
tags:
  - name: Authentication
    description: GitHub/OIDC OAuth and logout. Public routes (except logout).
  - name: Health
    description: Control-plane liveness and readiness.
  - name: Tasks
    description: Factory tasks — create, gate, cancel and retry.
  - name: Briefs
    description: Pre-factory business briefs (draft → in_review → approved).
  - name: Policies
    description: Policy and configuration overlay (does not rewrite t25.yaml).
  - name: Events
    description: Factory-floor SSE stream.
  - name: Agents
    description: CLI profiles by role.
security:
  - ApiKey: []
  - Session: []

components:
  securitySchemes:
    ApiKey:
      type: apiKey
      in: header
      name: X-T25-Key-Id
      description: Key ID. Secret in the Authorization Bearer header if the server requires it.
    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: Enabled OAuth flows
      security: []
      responses:
        '200':
          description: Which identity providers are enabled
          content:
            application/json:
              example:
                oauth: true
                github: true
                oidc: false
                google: false

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

  /api/auth/logout:
    post:
      tags: [Authentication]
      operationId: logout
      summary: End the session
      responses:
        '204':
          description: Cookie removed

  /api/v1/health:
    get:
      tags: [Health]
      operationId: getHealth
      summary: Liveness + adapters + queue
      responses:
        '200':
          description: Process is running
          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) reachable
      responses:
        '200':
          content:
            application/json:
              example: { ok: true }
        '503':
          description: control plane unavailable

  /api/v1/agents:
    get:
      tags: [Agents]
      operationId: listAgents
      summary: Configured roles and CLIs
      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: Update a role’s CLI chain (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: Role updated
        '400':
          description: Invalid role or providers

  /api/v1/tasks:
    get:
      tags: [Tasks]
      operationId: listTasks
      summary: List visible tasks
      responses:
        '200':
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/Task' }
              example:
                - { id: TASK-0042, title: Metrics endpoint, state: AWAITING_APPROVAL, risk: medium, projectId: default }
    post:
      tags: [Tasks]
      operationId: createTask
      summary: Open a task
      description: |
        Capability `operate`. The `Idempotency-Key` header returns the same task if the
        key was already used.
      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: Add metrics endpoint
              body: Expose latency by route
              type: feature
              risk: medium
              projectId: default
      responses:
        '201':
          description: Task created
          content:
            application/json:
              example:
                id: TASK-0042
                title: Add metrics endpoint
                state: RECEIVED
                risk: medium
                projectId: default

  /api/v1/tasks/{id}:
    get:
      tags: [Tasks]
      operationId: getTask
      summary: Task details + 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: Metrics endpoint, state: SPEC }
                runs: []
        '404':
          description: Task not found

  /api/v1/tasks/{id}/approve:
    post:
      tags: [Tasks]
      operationId: approveTask
      summary: Plan gate (go/no-go)
      description: Capability `approve`. Does not approve briefs or merge.
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Task advanced

  /api/v1/tasks/{id}/answer:
    post:
      tags: [Tasks]
      operationId: answerTask
      summary: Answer 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 left NEEDS_INPUT

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

  /api/v1/tasks/{id}/retry:
    post:
      tags: [Tasks]
      operationId: retryTask
      summary: Retry FAILED
      description: Honors max_retries and backoff. Returns 400 when retry is not available yet.
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Requeued
        '400':
          description: retry unavailable until …

  /api/v1/briefs:
    get:
      tags: [Briefs]
      operationId: listBriefs
      summary: List 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: Create draft
      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: The funnel loses people at the address
      responses:
        '201':
          description: Brief in draft

  /api/v1/briefs/{id}/intake:
    post:
      tags: [Briefs]
      operationId: intakeBrief
      summary: Brief approved → Task RECEIVED
      description: Only when status=approved. Idempotent when intakeTaskId already exists.
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Brief with intakeTaskId
        '409':
          description: Brief not approved yet

  /api/v1/policies/{projectId}:
    get:
      tags: [Policies]
      operationId: getPolicy
      summary: Effective project policy
      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: Policy overlay (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 saved

  /api/v1/config:
    get:
      tags: [Policies]
      operationId: getConfig
      summary: Sanitized config (without secrets)
      responses:
        '200':
          description: Effective t25.yaml + overlay
    put:
      tags: [Policies]
      operationId: putConfig
      summary: Config overlay (admin)
      requestBody:
        content:
          application/json:
            example:
              limits: { max_files_changed: 20 }
      responses:
        '200':
          description: Overlay saved

  /api/v1/events:
    get:
      tags: [Events]
      operationId: streamEvents
      summary: Factory-floor SSE
      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"}
