openapi: 3.0.3
info:
  title: CloudCLI REST API
  version: 0.2.0-draft
  description: |
    B12 — OpenAPI 3 spec (draft) for the CloudCLI backend (`server/`).

    Covers the REST surface mounted under `/api`. Endpoints are extracted from
    `server/routes/*.{js,ts}` and the mount table in `server/index.js`.

    ## Authentication
    Two mechanisms, both applied at `/api`:
    - **bearerAuth (JWT)** — `authenticateToken` middleware. Send
      `Authorization: Bearer <jwt>`. SSE endpoints (which cannot set headers)
      also accept the token as a `?token=` query param. When a token passes
      its half-life the server issues a fresh one in the `X-Refreshed-Token`
      response header — clients should adopt it.
      In **platform mode** (`IS_PLATFORM`) the single DB user is used and the
      JWT check is bypassed server-side.
    - **apiKeyAuth (x-api-key)** — optional `validateApiKey` middleware applied
      globally to `/api`. Only enforced when the `API_KEY` env var is set; then
      every `/api/*` request must also send `x-api-key: <API_KEY>`.

    Status: DRAFT. Request/response schemas are best-effort from route handlers;
    fields marked `(draft)` need confirmation against the implementation.

    ---

    ## 中文说明

    CloudCLI 后端（`server/`）的 OpenAPI 3 规范（草稿）。覆盖挂载在 `/api` 下的 REST
    接口，端点取自 `server/routes/*.{js,ts}` 与 `server/index.js` 的挂载表。

    ### 鉴权
    两种机制，均作用于 `/api`：
    - **bearerAuth（JWT）** —— `authenticateToken` 中间件。发送
      `Authorization: Bearer <jwt>`。SSE 端点（无法设置请求头）也接受把 token 作为
      `?token=` 查询参数。当 token 用过半个生命周期，服务端会在 `X-Refreshed-Token`
      响应头里下发新 token —— 客户端应采用它。在 **平台模式**（`IS_PLATFORM`）下，
      使用单一 DB 用户，服务端跳过 JWT 校验。
    - **apiKeyAuth（x-api-key）** —— 可选的 `validateApiKey` 中间件，全局作用于 `/api`。
      仅当设置了 `API_KEY` 环境变量时强制；此时每个 `/api/*` 请求还须发送
      `x-api-key: <API_KEY>`。

    状态：草稿。请求/响应 schema 为基于路由处理器的尽力而为；标 `(draft)` 的字段
    需对照实现确认。完整鉴权流程与可复制示例见 `apps/api-docs/auth-guide.zh.md`（中文）
    / `auth-guide.md`（English）。
  contact:
    name: cc01-03 (B12 API docs lane)
  license:
    name: AGPL-3.0-or-later
    url: https://www.gnu.org/licenses/agpl-3.0.html

servers:
  - url: /api
    description: Same-origin API root (all paths below are relative to /api)

tags:
  - name: auth
    description: Authentication & session (`/api/auth`) — no JWT required on register/login/status.
  - name: git
    description: Git operations for a project working tree (`/api/git`).
  - name: agent
    description: Claude agent command entry (`/api/agent`).
  - name: parallel-chat
    description: Multi-provider parallel chat fan-out + per-provider SSE streams (`/api/chat`).
  - name: mcp-utils
    description: MCP helper utilities (`/api/mcp-utils`).
  - name: plugins
    description: Plugin registry — list/manifest/assets/enable/install/update/remove (`/api/plugins`).
  - name: ralph
    description: RALPH autonomous-loop control (`/api/ralph`).
  - name: taskmaster
    description: Taskmaster PRD / task management (`/api/taskmaster`).
  - name: user
    description: User profile, git-config, onboarding (`/api/user`).
  - name: cc-instances
    description: CC instance fleet listing / status / RPC (`/api/cc-instances`).
  - name: projects
    description: Project listing / create / clone / star / rename / archive (`/api/projects`).
  - name: providers
    description: Per-provider auth/skills/MCP-servers + chat sessions (`/api/providers`).
  - name: settings
    description: API keys, credentials, notification & push prefs, server-env (`/api/settings`).
  - name: commands
    description: Slash-command listing & execution (`/api/commands`).
  - name: cursor
    description: Cursor CLI config (`/api/cursor`).
  - name: gemini
    description: Gemini session ops (`/api/gemini`).
  - name: agents-pipeline
    description: Multi-agent pipeline orchestration (`/api/agents`).

security:
  - bearerAuth: []
  - apiKeyAuth: []

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: |
        `Authorization: Bearer <jwt>`. SSE endpoints also accept `?token=<jwt>`.
        Response may carry `X-Refreshed-Token` with a renewed token.
    apiKeyAuth:
      type: apiKey
      in: header
      name: x-api-key
      description: Optional; only enforced when server has `API_KEY` env set.

  parameters:
    ProjectPathQuery:
      name: project
      in: query
      required: true
      schema: { type: string }
      description: Absolute path of the project working tree (handler reads query field `project`).
    ProjectIdPath:
      name: projectId
      in: path
      required: true
      schema: { type: string }

  responses:
    Unauthorized:
      description: Missing/invalid JWT or api key.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
          examples:
            noToken: { value: { error: "Access denied. No token provided." } }
            badKey: { value: { error: "Invalid API key" } }
    NotFound:
      description: Resource not found.
      content:
        application/json: { schema: { $ref: '#/components/schemas/Error' } }
    BadRequest:
      description: Validation error.
      content:
        application/json: { schema: { $ref: '#/components/schemas/Error' } }
    ServerError:
      description: Internal error.
      content:
        application/json: { schema: { $ref: '#/components/schemas/Error' } }

  schemas:
    Error:
      type: object
      properties:
        error: { type: string, description: Human-readable error message }
        code: { type: string, description: "Optional machine code (draft — backend rollout pending)" }
      required: [error]
    User:
      type: object
      properties:
        id: { type: integer }
        username: { type: string }
        role: { type: string, enum: [admin, user, guest] }
        status: { type: string, enum: [active, suspended] }
        git_name: { type: string, nullable: true }
        git_email: { type: string, nullable: true }
        has_completed_onboarding: { type: boolean }
        created_at: { type: string }
        last_login: { type: string, nullable: true }
    AuthResponse:
      type: object
      properties:
        token: { type: string, description: JWT }
        user: { $ref: '#/components/schemas/User' }
    Credentials:
      type: object
      required: [username, password]
      properties:
        username: { type: string, minLength: 3 }
        password: { type: string, minLength: 6 }
    ParallelChatRequest:
      type: object
      required: [message, providers]
      properties:
        message: { type: string }
        providers:
          type: array
          items: { type: string, enum: [claude, cursor, codex, gemini] }
        session_id: { type: string, nullable: true }
        project_path: { type: string, nullable: true }
    ParallelChatResponse:
      type: object
      properties:
        request_id: { type: string }
        streams:
          type: array
          items:
            type: object
            properties:
              provider: { type: string }
              stream_url: { type: string, description: "GET SSE url for this provider's stream" }
    ApiKey:
      type: object
      description: "User-issued API key. `api_key` is masked in list responses (first 10 chars + '...')."
      properties:
        id: { type: integer }
        name: { type: string }
        api_key: { type: string, description: "Masked in GET /api-keys; full value only returned once on creation" }
        enabled: { type: boolean }
        created_at: { type: string }
        last_used: { type: string, nullable: true }
    Project:
      type: object
      description: "Project with its sessions (draft — extra fields may exist)."
      properties:
        projectId: { type: string }
        name: { type: string }
        path: { type: string }
        starred: { type: boolean }
        sessions: { type: array, items: { type: object } }

paths:
  # ───────────────────────────── auth ─────────────────────────────
  /auth/status:
    get:
      operationId: getAuthStatus
      tags: [auth]
      security: []
      summary: Auth/system status (is a user registered, platform mode, etc.)
      responses:
        '500': { $ref: '#/components/responses/ServerError' }
        '200':
          description: Setup/auth status.
          content:
            application/json:
              schema:
                type: object
                properties:
                  needsSetup: { type: boolean, description: "true when no users exist yet (registration needed)" }
                  isAuthenticated: { type: boolean, description: "server-side always false; frontend overrides when a token is present" }
  /auth/register:
    post:
      operationId: postAuthRegister
      tags: [auth]
      security: []
      summary: Register the (first) user. May be blocked in single-user mode once taken.
      requestBody:
        required: true
        content:
          application/json: { schema: { $ref: '#/components/schemas/Credentials' } }
      responses:
        '200': { description: Registered, content: { application/json: { schema: { $ref: '#/components/schemas/AuthResponse' } } } }
        '400': { $ref: '#/components/responses/BadRequest' }
        '403': { description: "Single-user system already has an account", content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }
  /auth/login:
    post:
      operationId: postAuthLogin
      tags: [auth]
      security: []
      summary: Log in, returns JWT + user.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/Credentials' }
            example: { username: admin, password: "my-passphrase" }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/AuthResponse' }
              example:
                token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.<payload>.<sig>"
                user: { id: 1, username: admin }
        '401': { description: Invalid username or password, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }
  /auth/user:
    get:
      operationId: getAuthUser
      tags: [auth]
      summary: Current authenticated user.
      responses:
        '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/User' } } } }
        '401': { $ref: '#/components/responses/Unauthorized' }
  /auth/logout:
    post:
      operationId: postAuthLogout
      tags: [auth]
      summary: Log out (client discards token; server is stateless-JWT).
      responses:
        '401': { $ref: '#/components/responses/Unauthorized' }
        '200': { description: OK }

  # ───────────────────────────── git ─────────────────────────────
  /git/status:
    get:
      operationId: getGitStatus
      tags: [git]
      summary: Working-tree status for a project.
      parameters: [{ $ref: '#/components/parameters/ProjectPathQuery' }]
      responses:
        '200':
          description: Changed-file lists (additional fields e.g. staged/branch may also be present — draft).
          content:
            application/json:
              schema:
                type: object
                properties:
                  modified: { type: array, items: { type: string } }
                  untracked: { type: array, items: { type: string } }
                  staged: { type: array, items: { type: string }, description: "(draft) likely present" }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
  /git/diff:
    get:
      operationId: getGitDiff
      tags: [git]
      summary: Diff for a file (or whole tree).
      parameters:
        - { $ref: '#/components/parameters/ProjectPathQuery' }
        - { name: file, in: query, required: false, schema: { type: string } }
      responses:
        '200': { description: Unified diff text (draft) }
        '400': { $ref: '#/components/responses/BadRequest' }
  /git/file-with-diff:
    get:
      operationId: getGitFileWithDiff
      tags: [git]
      summary: File content alongside its diff.
      parameters:
        - { $ref: '#/components/parameters/ProjectPathQuery' }
        - { name: file, in: query, required: true, schema: { type: string } }
      responses: { '200': { description: OK (draft) }, '401': { $ref: '#/components/responses/Unauthorized' } }
  /git/initial-commit:
    post:
      operationId: postGitInitialCommit
      tags: [git]
      summary: Create the initial commit for a fresh repo.
      requestBody: { required: true, content: { application/json: { schema: { type: object, properties: { project: { type: string }, message: { type: string } }, required: [project] } } } }
      responses: { '200': { description: OK }, '409': { description: "Repository already has commits", content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } } }
  /git/commit:
    post:
      operationId: postGitCommit
      tags: [git]
      summary: Commit staged/selected files.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [project, message]
              properties:
                project: { type: string, description: "Project path (handler reads body field `project`, not project)" }
                message: { type: string }
                files: { type: array, items: { type: string }, description: "Optional subset; omit to commit all staged" }
      responses: { '200': { description: Commit created }, '400': { $ref: '#/components/responses/BadRequest' } }
  /git/revert-local-commit:
    post:
      operationId: postGitRevertLocalCommit
      tags: [git]
      summary: Revert the last local (unpushed) commit.
      requestBody: { required: true, content: { application/json: { schema: { type: object, properties: { project: { type: string } }, required: [project] } } } }
      responses: { '200': { description: OK }, '401': { $ref: '#/components/responses/Unauthorized' } }
  /git/branches:
    get:
      operationId: getGitBranches
      tags: [git]
      summary: List branches.
      parameters: [{ $ref: '#/components/parameters/ProjectPathQuery' }]
      responses:
        '401': { $ref: '#/components/responses/Unauthorized' }
        '200':
          description: Branch lists.
          content:
            application/json:
              schema:
                type: object
                properties:
                  branches: { type: array, items: { type: string } }
                  localBranches: { type: array, items: { type: string } }
                  remoteBranches: { type: array, items: { type: string } }
  /git/checkout:
    post:
      operationId: postGitCheckout
      tags: [git]
      summary: Checkout a branch.
      requestBody: { required: true, content: { application/json: { schema: { type: object, properties: { project: { type: string }, branch: { type: string } }, required: [project, branch] } } } }
      responses: { '200': { description: OK }, '400': { $ref: '#/components/responses/BadRequest' } }
  /git/create-branch:
    post:
      operationId: postGitCreateBranch
      tags: [git]
      summary: Create a branch.
      requestBody: { required: true, content: { application/json: { schema: { type: object, properties: { project: { type: string }, branch: { type: string } }, required: [project, branch] } } } }
      responses: { '200': { description: OK }, '401': { $ref: '#/components/responses/Unauthorized' } }
  /git/delete-branch:
    post:
      operationId: postGitDeleteBranch
      tags: [git]
      summary: Delete a branch (cannot delete current).
      requestBody: { required: true, content: { application/json: { schema: { type: object, properties: { project: { type: string }, branch: { type: string } }, required: [project, branch] } } } }
      responses: { '200': { description: OK }, '400': { description: "Cannot delete the currently checked-out branch", content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } } }
  /git/commits:
    get:
      operationId: getGitCommits
      tags: [git]
      summary: Commit history (optionally for one path).
      parameters:
        - { $ref: '#/components/parameters/ProjectPathQuery' }
        - { name: limit, in: query, required: false, schema: { type: integer, default: 10 } }
      responses:
        '401': { $ref: '#/components/responses/Unauthorized' }
        '200':
          description: Commit list.
          content:
            application/json:
              schema:
                type: object
                properties:
                  commits: { type: array, items: { type: object, description: "commit (hash/author/date/message — draft)" } }
  /git/commit-diff:
    get:
      operationId: getGitCommitDiff
      tags: [git]
      summary: Diff for a single commit.
      parameters:
        - { $ref: '#/components/parameters/ProjectPathQuery' }
        - { name: commit, in: query, required: true, schema: { type: string }, description: "Commit hash" }
      responses: { '200': { description: OK (draft) }, '401': { $ref: '#/components/responses/Unauthorized' } }
  /git/generate-commit-message:
    post:
      operationId: postGitGenerateCommitMessage
      tags: [git]
      summary: AI-generate a commit message from staged changes.
      requestBody: { required: true, content: { application/json: { schema: { type: object, properties: { project: { type: string } }, required: [project] } } } }
      responses: { '200': { description: "{ message }" }, '401': { $ref: '#/components/responses/Unauthorized' } }
  /git/remote-status:
    get:
      operationId: getGitRemoteStatus
      tags: [git]
      summary: Ahead/behind vs remote.
      parameters: [{ $ref: '#/components/parameters/ProjectPathQuery' }]
      responses: { '200': { description: OK (draft) }, '401': { $ref: '#/components/responses/Unauthorized' } }
  /git/fetch:
    post: { operationId: postGitFetch, tags: [git], summary: git fetch, requestBody: { required: true, content: { application/json: { schema: { type: object, properties: { project: { type: string } }, required: [project] } } } }, responses: { '200': { description: OK }, '400': { $ref: '#/components/responses/BadRequest' }, '401': { $ref: '#/components/responses/Unauthorized' } } }
  /git/pull:
    post: { operationId: postGitPull, tags: [git], summary: git pull, requestBody: { required: true, content: { application/json: { schema: { type: object, properties: { project: { type: string } }, required: [project] } } } }, responses: { '200': { description: OK }, '400': { $ref: '#/components/responses/BadRequest' }, '401': { $ref: '#/components/responses/Unauthorized' } } }
  /git/push:
    post: { operationId: postGitPush, tags: [git], summary: git push, requestBody: { required: true, content: { application/json: { schema: { type: object, properties: { project: { type: string } }, required: [project] } } } }, responses: { '200': { description: OK }, '400': { $ref: '#/components/responses/BadRequest' }, '401': { $ref: '#/components/responses/Unauthorized' } } }
  /git/publish:
    post: { operationId: postGitPublish, tags: [git], summary: Publish a local branch to remote (set upstream), requestBody: { required: true, content: { application/json: { schema: { type: object, properties: { project: { type: string }, branch: { type: string } }, required: [project] } } } }, responses: { '200': { description: OK }, '400': { $ref: '#/components/responses/BadRequest' }, '401': { $ref: '#/components/responses/Unauthorized' } } }
  /git/discard:
    post: { operationId: postGitDiscard, tags: [git], summary: Discard changes to tracked files, requestBody: { required: true, content: { application/json: { schema: { type: object, properties: { project: { type: string }, files: { type: array, items: { type: string } } }, required: [project] } } } }, responses: { '200': { description: OK }, '400': { $ref: '#/components/responses/BadRequest' }, '401': { $ref: '#/components/responses/Unauthorized' } } }
  /git/delete-untracked:
    post: { operationId: postGitDeleteUntracked, tags: [git], summary: Delete untracked files, requestBody: { required: true, content: { application/json: { schema: { type: object, properties: { project: { type: string }, files: { type: array, items: { type: string } } }, required: [project] } } } }, responses: { '200': { description: OK }, '400': { $ref: '#/components/responses/BadRequest' }, '401': { $ref: '#/components/responses/Unauthorized' } } }

  # ───────────────────────────── agent ─────────────────────────────
  /agent:
    post:
      operationId: postAgent
      tags: [agent]
      summary: Submit a Claude agent command (draft — body shape TBD against handler).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              description: "Run an agent. Either githubUrl (clone+run) or projectPath (existing tree)."
              properties:
                message: { type: string, description: "The prompt/task for the agent" }
                githubUrl: { type: string, description: "Repo to clone & run against (alt to projectPath)" }
                projectPath: { type: string, description: "Existing working tree to run against" }
                provider: { type: string, enum: [claude, cursor, codex, gemini], default: claude }
                model: { type: string }
                githubToken: { type: string }
                branchName: { type: string }
                sessionId: { type: string }
                stream: { type: boolean, default: true }
                cleanup: { type: boolean, default: true }
      responses:
        '200': { description: OK (draft) }
        '401': { $ref: '#/components/responses/Unauthorized' }

  # ─────────────────────────── parallel-chat ───────────────────────────
  /chat/parallel:
    post:
      operationId: postChatParallel
      tags: [parallel-chat]
      summary: Fan out a prompt to multiple providers; returns per-provider stream descriptors.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ParallelChatRequest' }
            example:
              message: "Refactor this function for readability"
              providers: [claude, gemini]
              project_path: /home/user/proj
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ParallelChatResponse' }
              example:
                request_id: req-7f3a
                streams:
                  - { provider: claude, stream_url: "/api/chat/parallel/stream/req-7f3a/claude" }
                  - { provider: gemini, stream_url: "/api/chat/parallel/stream/req-7f3a/gemini" }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
  /chat/parallel/stream/{requestId}/{provider}:
    get:
      operationId: getChatParallelStreamByRequestIdByProvider
      tags: [parallel-chat]
      summary: SSE stream for one provider of a parallel request.
      description: |
        `text/event-stream`. Each event is `data: <JSON>\n\n`. Control frames:
        `{type:'done'}`, `{type:'error', error}`, `{type:'session-id', ...}`;
        content frames carry the normalized message. EventSource cannot set
        headers — pass the JWT as `?token=`.
      parameters:
        - { name: requestId, in: path, required: true, schema: { type: string } }
        - { name: provider, in: path, required: true, schema: { type: string, enum: [claude, cursor, codex, gemini] } }
        - { name: token, in: query, required: false, schema: { type: string }, description: "JWT for SSE (header alternative)" }
      responses:
        '200':
          description: |
            SSE stream. Each event is `data: <JSON>\n\n`. Frame kinds (verified vs
            server/routes/parallel-chat.ts): control `{type:"session-id",sessionId}`,
            `{type:"error",error}`, `{type:"done"}`; otherwise a provider content chunk.
          content:
            text/event-stream:
              schema: { type: string }
              example: |
                data: {"type":"session-id","sessionId":"sess-9c2"}

                data: {"role":"assistant","content":"Sure, here's"}

                data: {"role":"assistant","content":" the refactor…"}

                data: {"type":"done"}
        '401': { $ref: '#/components/responses/Unauthorized' }
  /chat/parallel/{requestId}/abort:
    post:
      operationId: postChatParallelByRequestIdAbort
      tags: [parallel-chat]
      summary: Cancel every provider stream in a parallel request.
      parameters: [{ name: requestId, in: path, required: true, schema: { type: string } }]
      responses: { '200': { description: Aborted }, '401': { $ref: '#/components/responses/Unauthorized' } }

  # ───────────────────────────── mcp-utils ─────────────────────────────
  /mcp-utils/taskmaster-server:
    get:
      operationId: getMcpUtilsTaskmasterServer
      tags: [mcp-utils]
      summary: Taskmaster MCP server status/info.
      responses: { '200': { description: OK (draft) }, '401': { $ref: '#/components/responses/Unauthorized' } }

  # ───────────────────────────── plugins ─────────────────────────────
  /plugins:
    get:
      operationId: getPlugins
      tags: [plugins]
      summary: List installed plugins.
      responses:
        '401': { $ref: '#/components/responses/Unauthorized' }
        '200':
          description: Plugin list.
          content:
            application/json:
              schema:
                type: object
                properties:
                  plugins: { type: array, items: { type: object, description: "plugin (name/enabled/manifest — draft)" } }
  /plugins/{name}/manifest:
    get:
      operationId: getPluginsByNameManifest
      tags: [plugins]
      summary: Plugin manifest.
      parameters: [{ name: name, in: path, required: true, schema: { type: string } }]
      responses: { '200': { description: Manifest (draft) }, '404': { $ref: '#/components/responses/NotFound' } }
  /plugins/{name}/assets/{assetPath}:
    get:
      operationId: getPluginsByNameAssetsByAssetPath
      tags: [plugins]
      summary: Serve a plugin asset (wildcard path).
      parameters:
        - { name: name, in: path, required: true, schema: { type: string } }
        - { name: assetPath, in: path, required: true, schema: { type: string }, description: "Wildcard (*) asset path under the plugin" }
      responses: { '200': { description: Asset bytes }, '404': { $ref: '#/components/responses/NotFound' } }
  /plugins/{name}/enable:
    put:
      operationId: putPluginsByNameEnable
      tags: [plugins]
      summary: Enable/disable a plugin.
      parameters: [{ name: name, in: path, required: true, schema: { type: string } }]
      requestBody: { required: false, content: { application/json: { schema: { type: object, properties: { enabled: { type: boolean } } } } } }
      responses: { '401': { $ref: '#/components/responses/Unauthorized' }, '200': { description: OK }, '502': { description: "Plugin server error", content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } } }
  /plugins/install:
    post:
      operationId: postPluginsInstall
      tags: [plugins]
      summary: Install a plugin.
      requestBody: { required: true, content: { application/json: { schema: { type: object, properties: { url: { type: string, description: "Plugin source URL/identifier" } }, required: [url] } } } }
      responses: { '200': { description: Installed }, '400': { $ref: '#/components/responses/BadRequest' } }
  /plugins/{name}/update:
    post:
      operationId: postPluginsByNameUpdate
      tags: [plugins]
      summary: Update a plugin.
      parameters: [{ name: name, in: path, required: true, schema: { type: string } }]
      responses: { '200': { description: Updated }, '401': { $ref: '#/components/responses/Unauthorized' } }
  /plugins/{name}:
    delete:
      operationId: deletePluginsByName
      tags: [plugins]
      summary: Uninstall a plugin.
      parameters: [{ name: name, in: path, required: true, schema: { type: string } }]
      responses: { '200': { description: Removed }, '404': { $ref: '#/components/responses/NotFound' } }

  # ───────────────────────────── ralph ─────────────────────────────
  /ralph/{ralphId}/pause:
    post:
      operationId: postRalphByRalphIdPause
      tags: [ralph]
      summary: Pause a RALPH autonomous loop.
      parameters: [{ name: ralphId, in: path, required: true, schema: { type: string } }]
      responses: { '200': { description: Paused }, '404': { $ref: '#/components/responses/NotFound' } }
  /ralph/{ralphId}/resume:
    post:
      operationId: postRalphByRalphIdResume
      tags: [ralph]
      summary: Resume a paused RALPH loop.
      parameters: [{ name: ralphId, in: path, required: true, schema: { type: string } }]
      responses: { '200': { description: Resumed }, '401': { $ref: '#/components/responses/Unauthorized' } }
  /ralph/{ralphId}/inject:
    post:
      operationId: postRalphByRalphIdInject
      tags: [ralph]
      summary: Inject a message/instruction into a running RALPH loop.
      parameters: [{ name: ralphId, in: path, required: true, schema: { type: string } }]
      requestBody: { required: true, content: { application/json: { schema: { type: object, properties: { message: { type: string } }, required: [message] } } } }
      responses: { '200': { description: Injected }, '401': { $ref: '#/components/responses/Unauthorized' } }

  # ───────────────────────────── taskmaster ─────────────────────────────
  /taskmaster/installation-status:
    get: { operationId: getTaskmasterInstallationStatus, tags: [taskmaster], summary: Is taskmaster installed/available, responses: { '200': { description: "{ success, installation }", content: { application/json: { schema: { type: object, properties: { success: { type: boolean }, installation: { type: object, description: "installation status detail" } } } } } }, '401': { $ref: '#/components/responses/Unauthorized' } } }
  /taskmaster/tasks/{projectId}:
    get:
      operationId: getTaskmasterTasksByProjectId
      tags: [taskmaster]
      summary: List tasks for a project.
      parameters: [{ $ref: '#/components/parameters/ProjectIdPath' }]
      responses:
        '200':
          description: Tasks (empty list + message when no tasks.json).
          content:
            application/json:
              schema:
                type: object
                properties:
                  projectId: { type: string }
                  tasks: { type: array, items: { type: object } }
                  message: { type: string, description: "Present when tasks.json is missing" }
        '404': { $ref: '#/components/responses/NotFound' }
  /taskmaster/prd/{projectId}:
    get:
      operationId: getTaskmasterPrdByProjectId
      tags: [taskmaster]
      summary: Get the project's PRD.
      parameters: [{ $ref: '#/components/parameters/ProjectIdPath' }]
      responses: { '200': { description: PRD (draft) }, '401': { $ref: '#/components/responses/Unauthorized' } }
    post:
      operationId: postTaskmasterPrdByProjectId
      tags: [taskmaster]
      summary: Save/update the project's PRD.
      parameters: [{ $ref: '#/components/parameters/ProjectIdPath' }]
      requestBody: { required: true, content: { application/json: { schema: { type: object, properties: { content: { type: string } } } } } }
      responses: { '200': { description: Saved }, '401': { $ref: '#/components/responses/Unauthorized' } }
  /taskmaster/prd/{projectId}/{fileName}:
    get:
      operationId: getTaskmasterPrdByProjectIdByFileName
      tags: [taskmaster]
      summary: Get a named PRD file.
      parameters:
        - { $ref: '#/components/parameters/ProjectIdPath' }
        - { name: fileName, in: path, required: true, schema: { type: string } }
      responses: { '200': { description: OK (draft) }, '404': { $ref: '#/components/responses/NotFound' } }
  /taskmaster/init/{projectId}:
    post:
      operationId: postTaskmasterInitByProjectId
      tags: [taskmaster]
      summary: Initialize taskmaster in a project.
      parameters: [{ $ref: '#/components/parameters/ProjectIdPath' }]
      responses: { '200': { description: Initialized }, '401': { $ref: '#/components/responses/Unauthorized' } }
  /taskmaster/add-task/{projectId}:
    post:
      operationId: postTaskmasterAddTaskByProjectId
      tags: [taskmaster]
      summary: Add a task.
      parameters: [{ $ref: '#/components/parameters/ProjectIdPath' }]
      requestBody: { required: true, content: { application/json: { schema: { type: object, properties: { prompt: { type: string, description: "Natural-language task (taskmaster generates the task)" }, title: { type: string }, description: { type: string }, priority: { type: string, default: medium }, dependencies: { type: array, items: { type: string } } } } } } }
      responses: { '200': { description: Added }, '401': { $ref: '#/components/responses/Unauthorized' } }
  /taskmaster/update-task/{projectId}/{taskId}:
    put:
      operationId: putTaskmasterUpdateTaskByProjectIdByTaskId
      tags: [taskmaster]
      summary: Update a task.
      parameters:
        - { $ref: '#/components/parameters/ProjectIdPath' }
        - { name: taskId, in: path, required: true, schema: { type: string } }
      requestBody: { required: true, content: { application/json: { schema: { type: object } } } }
      responses: { '200': { description: Updated }, '401': { $ref: '#/components/responses/Unauthorized' } }
  /taskmaster/parse-prd/{projectId}:
    post:
      operationId: postTaskmasterParsePrdByProjectId
      tags: [taskmaster]
      summary: Parse the PRD into tasks.
      parameters: [{ $ref: '#/components/parameters/ProjectIdPath' }]
      responses: { '200': { description: Parsed }, '401': { $ref: '#/components/responses/Unauthorized' } }
  /taskmaster/prd-templates:
    get: { operationId: getTaskmasterPrdTemplates, tags: [taskmaster], summary: List PRD templates, responses: { '200': { description: OK (draft) }, '401': { $ref: '#/components/responses/Unauthorized' } } }
  /taskmaster/apply-template/{projectId}:
    post:
      operationId: postTaskmasterApplyTemplateByProjectId
      tags: [taskmaster]
      summary: Apply a PRD template to a project.
      parameters: [{ $ref: '#/components/parameters/ProjectIdPath' }]
      requestBody: { required: true, content: { application/json: { schema: { type: object, properties: { templateId: { type: string } } } } } }
      responses: { '200': { description: Applied }, '401': { $ref: '#/components/responses/Unauthorized' } }

  # ───────────────────────────── user ─────────────────────────────
  /user/git-config:
    get:
      operationId: getUserGitConfig
      tags: [user]
      summary: Get the user's git name/email.
      responses:
        '200':
          description: Git config.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  gitName: { type: string, nullable: true }
                  gitEmail: { type: string, nullable: true }
        '401': { $ref: '#/components/responses/Unauthorized' }
    post:
      operationId: postUserGitConfig
      tags: [user]
      summary: Set the user's git name/email.
      requestBody: { required: true, content: { application/json: { schema: { type: object, properties: { git_name: { type: string }, git_email: { type: string } } } } } }
      responses: { '200': { description: Saved }, '401': { $ref: '#/components/responses/Unauthorized' } }
  /user/complete-onboarding:
    post: { operationId: postUserCompleteOnboarding, tags: [user], summary: Mark onboarding complete, responses: { '200': { description: OK }, '401': { $ref: '#/components/responses/Unauthorized' } } }
  /user/onboarding-status:
    get:
      operationId: getUserOnboardingStatus
      tags: [user]
      summary: Onboarding completion flag.
      responses:
        '401': { $ref: '#/components/responses/Unauthorized' }
        '200':
          description: Onboarding status.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  hasCompletedOnboarding: { type: boolean }

  # ───────────────────────────── cc-instances ─────────────────────────────
  /cc-instances/list:
    get:
      operationId: getCcInstancesList
      tags: [cc-instances]
      summary: List known CC instances.
      responses: { '200': { description: "{ ok, instances[] }", content: { application/json: { schema: { type: object, properties: { ok: { type: boolean }, instances: { type: array, items: { type: object } } } } } } }, '401': { $ref: '#/components/responses/Unauthorized' } }
  /cc-instances/{ccId}/status:
    get:
      operationId: getCcInstancesByCcIdStatus
      tags: [cc-instances]
      summary: Status for one CC instance.
      parameters: [{ name: ccId, in: path, required: true, schema: { type: string, pattern: '^cc\d{2}-\d{2}$' } }]
      responses: { '200': { description: Status (draft) }, '400': { $ref: '#/components/responses/BadRequest' }, '404': { $ref: '#/components/responses/NotFound' } }
  /cc-instances/{ccId}/rpc:
    post:
      operationId: postCcInstancesByCcIdRpc
      tags: [cc-instances]
      summary: Send an RPC to a CC instance.
      parameters: [{ name: ccId, in: path, required: true, schema: { type: string, pattern: '^cc\d{2}-\d{2}$' } }]
      requestBody: { required: true, content: { application/json: { schema: { type: object, properties: { command: { type: string, description: "RPC command/method name the CC instance handles" } }, required: [command] } } } }
      responses: { '200': { description: RPC result (draft) }, '400': { $ref: '#/components/responses/BadRequest' } }

  # ───────────────────────────── projects ─────────────────────────────
  /projects:
    get:
      operationId: getProjects
      tags: [projects]
      summary: List active projects (with their sessions). Returns a bare array.
      responses:
        '200':
          description: Project array.
          content:
            application/json:
              schema: { type: array, items: { $ref: '#/components/schemas/Project' } }
        '401': { $ref: '#/components/responses/Unauthorized' }
  /projects/archived:
    get: { operationId: getProjectsArchived, tags: [projects], summary: List archived projects, responses: { '200': { description: OK (draft) }, '401': { $ref: '#/components/responses/Unauthorized' } } }
  /projects/{projectId}/sessions:
    get:
      operationId: getProjectsByProjectIdSessions
      tags: [projects]
      summary: Sessions for a project.
      parameters: [{ $ref: '#/components/parameters/ProjectIdPath' }]
      responses: { '200': { description: Session list (draft) }, '401': { $ref: '#/components/responses/Unauthorized' } }
  /projects/create-project:
    post:
      operationId: postProjectsCreateProject
      tags: [projects]
      summary: Create or clone a project.
      requestBody: { required: true, content: { application/json: { schema: { type: object, properties: { name: { type: string }, path: { type: string }, repoUrl: { type: string, description: "Optional git URL to clone (https:// or git@)" } } } } } }
      responses: { '200': { description: Created }, '400': { $ref: '#/components/responses/BadRequest' } }
  /projects/migrate-legacy-stars:
    post: { operationId: postProjectsMigrateLegacyStars, tags: [projects], summary: One-off migrate legacy starred projects, responses: { '200': { description: OK }, '401': { $ref: '#/components/responses/Unauthorized' } } }
  /projects/clone-progress:
    get:
      operationId: getProjectsCloneProgress
      tags: [projects]
      summary: Clone progress (poll while a clone runs).
      parameters: [{ name: id, in: query, required: false, schema: { type: string } }]
      responses: { '200': { description: Progress (draft) }, '401': { $ref: '#/components/responses/Unauthorized' } }
  /projects/{projectId}/taskmaster:
    get:
      operationId: getProjectsByProjectIdTaskmaster
      tags: [projects]
      summary: Taskmaster status for a project.
      parameters: [{ $ref: '#/components/parameters/ProjectIdPath' }]
      responses: { '200': { description: OK (draft) }, '401': { $ref: '#/components/responses/Unauthorized' } }
  /projects/{projectId}/rename:
    put:
      operationId: putProjectsByProjectIdRename
      tags: [projects]
      summary: Rename a project (display name).
      parameters: [{ $ref: '#/components/parameters/ProjectIdPath' }]
      requestBody: { required: true, content: { application/json: { schema: { type: object, properties: { name: { type: string } }, required: [name] } } } }
      responses: { '200': { description: Renamed }, '401': { $ref: '#/components/responses/Unauthorized' } }
  /projects/{projectId}/toggle-star:
    post:
      operationId: postProjectsByProjectIdToggleStar
      tags: [projects]
      summary: Star/unstar a project.
      parameters: [{ $ref: '#/components/parameters/ProjectIdPath' }]
      responses: { '200': { description: OK }, '401': { $ref: '#/components/responses/Unauthorized' } }
  /projects/{projectId}/restore:
    post:
      operationId: postProjectsByProjectIdRestore
      tags: [projects]
      summary: Restore an archived project.
      parameters: [{ $ref: '#/components/parameters/ProjectIdPath' }]
      responses: { '200': { description: Restored }, '401': { $ref: '#/components/responses/Unauthorized' } }
  /projects/{projectId}:
    delete:
      operationId: deleteProjectsByProjectId
      tags: [projects]
      summary: Archive/delete a project.
      parameters: [{ $ref: '#/components/parameters/ProjectIdPath' }]
      responses: { '200': { description: OK }, '404': { $ref: '#/components/responses/NotFound' } }

  # ───────────────────────────── providers ─────────────────────────────
  /providers/{provider}/auth/status:
    get:
      operationId: getProvidersByProviderAuthStatus
      tags: [providers]
      summary: Auth status for a provider (claude/cursor/codex/gemini).
      parameters: [{ name: provider, in: path, required: true, schema: { type: string } }]
      responses: { '200': { description: OK (draft) }, '401': { $ref: '#/components/responses/Unauthorized' } }
  /providers/{provider}/skills:
    get:
      operationId: getProvidersByProviderSkills
      tags: [providers]
      summary: Skills/slash-commands surfaced by a provider.
      parameters: [{ name: provider, in: path, required: true, schema: { type: string } }]
      responses: { '200': { description: OK (draft) }, '401': { $ref: '#/components/responses/Unauthorized' } }
  /providers/{provider}/mcp/servers:
    get:
      operationId: getProvidersByProviderMcpServers
      tags: [providers]
      summary: List a provider's MCP servers.
      parameters: [{ name: provider, in: path, required: true, schema: { type: string } }]
      responses: { '200': { description: OK (draft) }, '401': { $ref: '#/components/responses/Unauthorized' } }
    post:
      operationId: postProvidersByProviderMcpServers
      tags: [providers]
      summary: Add an MCP server for a provider.
      parameters: [{ name: provider, in: path, required: true, schema: { type: string } }]
      requestBody: { required: true, content: { application/json: { schema: { type: object, properties: { name: { type: string }, config: { type: object } }, required: [name] } } } }
      responses: { '200': { description: Added }, '401': { $ref: '#/components/responses/Unauthorized' } }
  /providers/{provider}/mcp/servers/{name}:
    delete:
      operationId: deleteProvidersByProviderMcpServersByName
      tags: [providers]
      summary: Remove a provider MCP server.
      parameters:
        - { name: provider, in: path, required: true, schema: { type: string } }
        - { name: name, in: path, required: true, schema: { type: string } }
      responses: { '200': { description: Removed }, '401': { $ref: '#/components/responses/Unauthorized' } }
  /providers/mcp/servers/global:
    post:
      operationId: postProvidersMcpServersGlobal
      tags: [providers]
      summary: Add a global (all-provider) MCP server.
      requestBody: { required: true, content: { application/json: { schema: { type: object, properties: { name: { type: string }, config: { type: object } }, required: [name] } } } }
      responses: { '200': { description: Added }, '401': { $ref: '#/components/responses/Unauthorized' } }
  /providers/sessions/archived:
    get: { operationId: getProvidersSessionsArchived, tags: [providers], summary: List archived chat sessions, responses: { '200': { description: OK (draft) }, '401': { $ref: '#/components/responses/Unauthorized' } } }
  /providers/sessions/{sessionId}:
    delete:
      operationId: deleteProvidersSessionsBySessionId
      tags: [providers]
      summary: Archive/delete a chat session.
      parameters: [{ name: sessionId, in: path, required: true, schema: { type: string } }]
      responses: { '200': { description: OK }, '401': { $ref: '#/components/responses/Unauthorized' } }
    put:
      operationId: putProvidersSessionsBySessionId
      tags: [providers]
      summary: Update a chat session (e.g. rename).
      parameters: [{ name: sessionId, in: path, required: true, schema: { type: string } }]
      requestBody: { required: true, content: { application/json: { schema: { type: object, properties: { title: { type: string } } } } } }
      responses: { '200': { description: Updated }, '401': { $ref: '#/components/responses/Unauthorized' } }
  /providers/sessions/{sessionId}/restore:
    post:
      operationId: postProvidersSessionsBySessionIdRestore
      tags: [providers]
      summary: Restore an archived session.
      parameters: [{ name: sessionId, in: path, required: true, schema: { type: string } }]
      responses: { '200': { description: Restored }, '401': { $ref: '#/components/responses/Unauthorized' } }
  /providers/sessions/{sessionId}/messages:
    get:
      operationId: getProvidersSessionsBySessionIdMessages
      tags: [providers]
      summary: Messages of a chat session.
      parameters: [{ name: sessionId, in: path, required: true, schema: { type: string } }]
      responses: { '200': { description: Message list (draft) }, '401': { $ref: '#/components/responses/Unauthorized' } }
  /providers/search/sessions:
    get:
      operationId: getProvidersSearchSessions
      tags: [providers]
      summary: Search across chat sessions.
      parameters: [{ name: q, in: query, required: false, schema: { type: string } }]
      responses: { '200': { description: Search results (draft) }, '401': { $ref: '#/components/responses/Unauthorized' } }

  # ───────────────────────────── settings ─────────────────────────────
  /settings/api-keys:
    get:
      operationId: getSettingsApiKeys
      tags: [settings]
      summary: List the user's API keys (the x-api-key style keys this server issues/accepts).
      responses:
        '401': { $ref: '#/components/responses/Unauthorized' }
        '200':
          description: API key list (secrets masked).
          content:
            application/json:
              schema:
                type: object
                properties:
                  apiKeys: { type: array, items: { $ref: '#/components/schemas/ApiKey' } }
    post:
      operationId: postSettingsApiKeys
      tags: [settings]
      summary: Create an API key.
      requestBody: { required: true, content: { application/json: { schema: { type: object, properties: { name: { type: string } } } } } }
      responses: { '200': { description: "Created (key shown once)" }, '401': { $ref: '#/components/responses/Unauthorized' } }
  /settings/api-keys/{keyId}:
    delete:
      operationId: deleteSettingsApiKeysByKeyId
      tags: [settings]
      summary: Delete an API key.
      parameters: [{ name: keyId, in: path, required: true, schema: { type: string } }]
      responses: { '200': { description: Deleted }, '401': { $ref: '#/components/responses/Unauthorized' } }
  /settings/api-keys/{keyId}/toggle:
    patch:
      operationId: patchSettingsApiKeysByKeyIdToggle
      tags: [settings]
      summary: Enable/disable an API key.
      parameters: [{ name: keyId, in: path, required: true, schema: { type: string } }]
      responses: { '200': { description: Toggled }, '401': { $ref: '#/components/responses/Unauthorized' } }
  /settings/credentials:
    get: { operationId: getSettingsCredentials, tags: [settings], summary: List stored provider credentials (masked), responses: { '200': { description: OK (draft) }, '401': { $ref: '#/components/responses/Unauthorized' } } }
    post:
      operationId: postSettingsCredentials
      tags: [settings]
      summary: Store a provider credential / API key.
      requestBody: { required: true, content: { application/json: { schema: { type: object, properties: { credentialName: { type: string }, credentialType: { type: string }, credentialValue: { type: string }, description: { type: string } }, required: [credentialName, credentialType, credentialValue] } } } }
      responses: { '200': { description: Stored }, '400': { $ref: '#/components/responses/BadRequest' } }
  /settings/credentials/{credentialId}:
    delete:
      operationId: deleteSettingsCredentialsByCredentialId
      tags: [settings]
      summary: Delete a credential.
      parameters: [{ name: credentialId, in: path, required: true, schema: { type: string } }]
      responses: { '200': { description: Deleted }, '401': { $ref: '#/components/responses/Unauthorized' } }
  /settings/credentials/{credentialId}/toggle:
    patch:
      operationId: patchSettingsCredentialsByCredentialIdToggle
      tags: [settings]
      summary: Enable/disable a credential.
      parameters: [{ name: credentialId, in: path, required: true, schema: { type: string } }]
      responses: { '200': { description: Toggled }, '401': { $ref: '#/components/responses/Unauthorized' } }
  /settings/notification-preferences:
    get: { operationId: getSettingsNotificationPreferences, tags: [settings], summary: Get notification preferences, responses: { '200': { description: OK (draft) }, '401': { $ref: '#/components/responses/Unauthorized' } } }
    put:
      operationId: putSettingsNotificationPreferences
      tags: [settings]
      summary: Update notification preferences.
      requestBody: { required: true, content: { application/json: { schema: { type: object } } } }
      responses: { '200': { description: Updated }, '401': { $ref: '#/components/responses/Unauthorized' } }
  /settings/push/vapid-public-key:
    get: { operationId: getSettingsPushVapidPublicKey, tags: [settings], summary: Web-push VAPID public key, responses: { '200': { description: "{ publicKey }" }, '401': { $ref: '#/components/responses/Unauthorized' } } }
  /settings/push/subscribe:
    post:
      operationId: postSettingsPushSubscribe
      tags: [settings]
      summary: Register a web-push subscription.
      requestBody: { required: true, content: { application/json: { schema: { type: object, description: "PushSubscription JSON" } } } }
      responses: { '200': { description: Subscribed }, '401': { $ref: '#/components/responses/Unauthorized' } }
  /settings/push/unsubscribe:
    post:
      operationId: postSettingsPushUnsubscribe
      tags: [settings]
      summary: Remove a web-push subscription.
      requestBody: { required: true, content: { application/json: { schema: { type: object } } } }
      responses: { '200': { description: Unsubscribed }, '401': { $ref: '#/components/responses/Unauthorized' } }
  /settings/server-env:
    get:
      operationId: getSettingsServerEnv
      tags: [settings]
      summary: Non-secret server environment/feature flags surfaced to the UI.
      responses: { '200': { description: "{ platform }", content: { application/json: { schema: { type: object, properties: { platform: { type: string, description: "process.platform, e.g. linux/darwin/win32" } } } } } }, '401': { $ref: '#/components/responses/Unauthorized' } }

  # ───────────────────────────── commands ─────────────────────────────
  /commands/list:
    post:
      operationId: postCommandsList
      tags: [commands]
      summary: List available slash-commands (POST — body carries project/provider context).
      requestBody: { required: false, content: { application/json: { schema: { type: object, properties: { projectPath: { type: string }, provider: { type: string } } } } } }
      responses: { '200': { description: Command list (draft) }, '401': { $ref: '#/components/responses/Unauthorized' } }
  /commands/execute:
    post:
      operationId: postCommandsExecute
      tags: [commands]
      summary: Execute a slash-command.
      requestBody: { required: true, content: { application/json: { schema: { type: object, properties: { commandName: { type: string }, commandPath: { type: string }, args: { type: array, items: { type: string } }, context: { type: object } }, required: [commandName] } } } }
      responses: { '200': { description: OK (draft) }, '400': { $ref: '#/components/responses/BadRequest' } }

  # ───────────────────────────── cursor ─────────────────────────────
  /cursor/config:
    get:
      operationId: getCursorConfig
      tags: [cursor]
      summary: Cursor CLI config/availability.
      responses: { '200': { description: OK (draft) }, '401': { $ref: '#/components/responses/Unauthorized' } }

  # ───────────────────────────── gemini ─────────────────────────────
  /gemini/sessions/{sessionId}:
    delete:
      operationId: deleteGeminiSessionsBySessionId
      tags: [gemini]
      summary: Delete a Gemini session.
      parameters: [{ name: sessionId, in: path, required: true, schema: { type: string } }]
      responses: { '200': { description: Deleted }, '401': { $ref: '#/components/responses/Unauthorized' } }

  # ───────────────────────────── agents-pipeline ─────────────────────────────
  /agents/list:
    get:
      operationId: getAgentsList
      tags: [agents-pipeline]
      summary: List available pipeline agents.
      responses: { '200': { description: Agent list (draft) }, '401': { $ref: '#/components/responses/Unauthorized' } }
  /agents/pipeline:
    post:
      operationId: postAgentsPipeline
      tags: [agents-pipeline]
      summary: Start a multi-agent pipeline run.
      requestBody: { required: true, content: { application/json: { schema: { type: object, properties: { task: { type: string }, pipeline: { type: array, items: { type: string }, description: "ordered agent/stage ids" }, project_path: { type: string } }, required: [task] } } } }
      responses: { '200': { description: "{ taskId }" }, '400': { $ref: '#/components/responses/BadRequest' } }
  /agents/pipeline/{taskId}:
    get:
      operationId: getAgentsPipelineByTaskId
      tags: [agents-pipeline]
      summary: Pipeline run status/result.
      parameters: [{ name: taskId, in: path, required: true, schema: { type: string } }]
      responses: { '200': { description: OK (draft) }, '404': { $ref: '#/components/responses/NotFound' } }
