openapi: 3.1.1
info:
  title: Assign API
  version: 1.0.0-rc.9
  description: Public HTTP API for Assign. The document release is independent from server, client, SDK, realtime, and MCP releases.
  license:
    name: Proprietary
    identifier: LicenseRef-Proprietary
x-assign-api-major: 1
servers:
  - url: https://api.assign.so
    description: Production
tags:
  - name: HomeWork
    x-assign-status: upcoming
    description: Opt-in composed Home and My Work baselines. Upcoming; not deployed.
  - name: Realtime
    description: Browser application WebSocket admission and authorized replay custody. Upcoming; not deployed.
  - name: Authentication
    description: Browser-session and native OAuth credential lifecycle operations.
  - name: Account
    description: The current user and their Workspace memberships.
  - name: Workspaces
    description: Workspace lifecycle, membership, and invitations.
  - name: Projects
    description: Workspace Projects and their workflow Statuses.
  - name: Tasks
    description: Project Tasks.
  - name: Integrations
    description: Provider catalog metadata and live external entities linked to Assign resources.
  - name: Comments
    description: Task discussion.
  - name: Documents
    description: Workspace rich-text Documents and their versioned content.
  - name: Attachments
    description: Direct-to-object-storage file uploads and downloads.
  - name: Search
    description: Bounded Workspace-scoped content search.
  - name: Knowledge
    description: Evidence-backed Workspace Knowledge retrieval projected through Core.
  - name: Discuss
    description: Membership-private persistent conversation with the shared Search Agent.
  - name: CLI
    description: Bearer-authenticated, code-addressed projections and commands for the Assign CLI.
  - name: Developer clients
    description: Shared first-party developer-client authorization and credential-bound Workspace operations.
  - name: Time tracking
    description: Manual Task time entries, Workspace rounding policy, and personal timesheets.
  - name: Billing
    description: Provider-neutral Workspace subscription status and invoice references.
  - name: Activity
    description: Upcoming versioned grouped Activity companion reads.
  - name: Agents
    description: Curated Agent definitions and Workspace-owned desired Agent configuration. Execution remains in the private Knowledge service.
paths:
  /api/v1/workspaces/{workspace_id}/home-work-baseline:
    get:
      tags:
        - HomeWork
      operationId: getHomeWorkWorkspaceBaseline
      summary: Read an opt-in Home/Work composed baseline
      description: Upcoming; not yet deployed. Captures all required reads and enrichment in one short repeatable-read transaction per scope, with independent authorization and cursor custody. Account and Workspace responses are not one global atomic snapshot. No actor selector is accepted. The entire encoded response is bounded to 1,048,576 bytes; required catalogs must be complete (at most 100 items), Task lists at most 100 items and 1,000 label references per list. Errors contain neither partial data nor a realtime cursor. Ordinary reads remain unchanged.
      security: &a1
        - browserSession: []
        - nativeAccessToken: []
        - bearerToken: []
        - developerAccessToken: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - name: view
          in: query
          required: true
          schema:
            $ref: "#/components/schemas/MyWorkView"
        - $ref: "#/components/parameters/Cursor"
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 100
        - name: following
          in: query
          schema:
            type: boolean
            default: false
          description: Demand the first bounded Following page; false returns null.
      responses:
        "200":
          description: Complete independently authorized scope snapshot. No partial response or cursor is returned on failure.
          headers:
            Cache-Control:
              required: true
              schema:
                type: string
                enum:
                  - no-store
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/HomeWorkWorkspaceBaseline"
        "400": &a2
          $ref: "#/components/responses/BadRequest"
        "401": &a3
          $ref: "#/components/responses/Unauthorized"
        "404": &a4
          $ref: "#/components/responses/NotFound"
        "409": &a5
          description: Incomplete catalog, enrichment/reference bound or encoded response exceeding 1,048,576 bytes. Error code baseline_bounds; no partial composition or cursor.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "500": &a6
          $ref: "#/components/responses/InternalServerError"
        "503": &a7
          description: Snapshot deadline (two seconds), source/authority capture unavailable (baseline_unavailable), or existing maintenance unavailability. No partial composition or cursor.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /api/v1/me/home-work-baseline:
    get:
      tags:
        - HomeWork
      operationId: getHomeWorkAccountBaseline
      summary: Read an opt-in Home/Work composed baseline
      description: Upcoming; not yet deployed. Captures all required reads and enrichment in one short repeatable-read transaction per scope, with independent authorization and cursor custody. Account and Workspace responses are not one global atomic snapshot. No actor selector is accepted. The entire encoded response is bounded to 1,048,576 bytes; required catalogs must be complete (at most 100 items), Task lists at most 100 items and 1,000 label references per list. Errors contain neither partial data nor a realtime cursor. Ordinary reads remain unchanged.
      security: *a1
      parameters:
        - name: workspace_id
          in: query
          required: true
          schema:
            $ref: "#/components/schemas/Identifier"
          description: Admitted selected Workspace; mismatch fails the whole composition.
      responses:
        "200":
          description: Complete independently authorized scope snapshot. No partial response or cursor is returned on failure.
          headers:
            Cache-Control:
              required: true
              schema:
                type: string
                enum:
                  - no-store
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/HomeWorkAccountBaseline"
        "400": *a2
        "401": *a3
        "404": *a4
        "409": *a5
        "500": *a6
        "503": *a7
  /api/v1/realtime:
    get:
      tags:
        - Realtime
      operationId: openApplicationRealtime
      summary: Upgrade an authenticated application socket
      description: |
        Upcoming, not deployed. Browser admission requires the current session,
        exactly one configured browser Origin, no Authorization header, and the
        WebSocket subprotocol assign.realtime.v1. Native mobile admission requires
        one native access-token Authorization bearer, no Origin or Cookie header,
        and the same subprotocol. The optional X-Assign-Workspace-ID header selects
        a current authorized Workspace for native Workspace scopes; omit it for
        Account-only scopes. No query parameters or URL credentials are accepted.
        IDE admission also accepts a developer OAuth bearer issued to
        assign-jetbrains or assign-vscode with assign:read. It forbids Origin,
        Cookie and URL credentials. The grant fixes the Workspace; an optional
        X-Assign-Workspace-ID must match it. IDE tokens admit only workspace,
        projects and task_comments scopes, never Account or Discuss scopes.
        CLI, MCP and personal API tokens do not admit application sockets.
        Every active subscription rechecks the exact access token, grant, client,
        scope and current membership; reconnect after expiry/revocation requires
        valid authentication and new baseline custody.
        This operation is a WebSocket upgrade, not a JSON GET. HTTP SDK methods do
        not implement the upgraded transport; use the application WebSocket owner.
        JSON control frames negotiate bounded subscriptions and independent applied
        ACKs. Account/security scopes omit workspace_id and carry no client-supplied
        User identity. Inbox and personal Project scopes require workspace_id.
        A visible application owner may send presence_renew with an applied
        Workspace subscription ID and cursor. It renews only that owner's
        eligible human Workspace lease; transport pongs do not renew presence.
        An applied Workspace owner may send presence_observe with that cursor and
        up to 256 distinct active human member User IDs. An empty list clears
        observation. Authorized snapshots carry an epoch, revision and complete
        selected online states; presence_unknown clears confidence on source,
        observer or authorization loss. Wake recovery requires a fresh snapshot.
        Ordinary Workspace subscriptions exclude private collection hints.
        The discuss scope uses a separate session-bound private sequence and
        content-free message identity hints; its cursor is not a Workspace
        journal position.
        Source health is separate from canonical applied state. See the public
        realtime guide for authentication, scope and recovery rules.
      security:
        - browserSession: []
        - nativeAccessToken: []
        - developerAccessToken: []
      parameters:
        - name: Origin
          in: header
          required: false
          description: Required for browser sessions; forbidden for native bearer admission.
          schema:
            type: string
            format: uri
        - name: X-Assign-Workspace-ID
          in: header
          required: false
          description: Native bearer Workspace selection; resolved against current membership.
          schema:
            type: string
            format: uuid
        - name: Sec-WebSocket-Protocol
          in: header
          required: true
          schema:
            type: string
            enum:
              - assign.realtime.v1
      responses:
        "101":
          description: WebSocket established; versioned control frames follow.
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "503":
          description: Admission capacity is temporarily unavailable; retry with bounded backoff.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /api/v1/realtime/baseline:
    get:
      tags:
        - Realtime
      operationId: getApplicationRealtimeBaseline
      summary: Capture authorized realtime routing custody or a domain page
      description: |
        Upcoming, not deployed. Omitted scope captures ordinary Workspace routing
        custody. The account, account_security, inbox and personal_projects scopes
        accept only the scope query parameter and return content-free routing
        custody. The discuss scope returns private conversation sequence custody
        without message content or an atomic transcript snapshot. They do not
        establish an atomic domain snapshot. Account scopes
        work without a Workspace. All private cursors bind the current User and
        browser session or native access-token identity; Workspace-private cursors
        also bind current membership. Native mobile callers use a single bearer
        with no Origin or Cookie, and select a Workspace with
        X-Assign-Workspace-ID when the scope requires one. Projects and
        task_comments return a canonical page and its same-snapshot
        cursor. task_comments requires a canonical UUID resource_id and admits at
        most two items. Canonical private reads use X-Assign-Realtime-Baseline
        instead. IDE developer bearers (assign-jetbrains or assign-vscode)
        admit only omitted scope/workspace custody, projects and task_comments.
        Their grant fixes the Workspace; an optional Workspace header must match.
        They omit Origin and Cookie. Private Account and Discuss baselines are
        denied. Unknown or duplicate query parameters are rejected.
      security:
        - browserSession: []
        - nativeAccessToken: []
        - developerAccessToken: []
      parameters:
        - name: X-Assign-Workspace-ID
          in: header
          required: false
          description: Native bearer Workspace selection; omit for Account-only scopes.
          schema:
            type: string
            format: uuid
        - name: scope
          in: query
          schema:
            type: string
            enum:
              - projects
              - task_comments
              - account
              - account_security
              - inbox
              - personal_projects
              - discuss
        - name: resource_id
          in: query
          description: Task UUID, required only for task_comments.
          schema:
            type: string
            format: uuid
        - name: continuation
          in: query
          description: Opaque signed domain-page cursor; only projects or task_comments.
          schema:
            type: string
            maxLength: 4096
        - name: limit
          in: query
          description: Domain-page item bound; projects allows 1–100 and task_comments allows 1–2.
          schema:
            type: integer
            minimum: 1
            maximum: 100
      responses:
        "200":
          description: Authorized routing custody or canonical domain page; never reusable after session/authority loss.
          headers:
            Cache-Control:
              schema:
                type: string
                const: no-store
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RealtimeBaseline"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "409":
          $ref: "#/components/responses/Conflict"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "503":
          description: Canonical baseline temporarily unavailable; do not publish partial state.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /oauth/register:
    post:
      tags:
        - Identity
      operationId: registerMCPNativeClient
      summary: Register a public native MCP OAuth client
      description: |
        Registers a client with HTTP loopback callback URIs. Registration grants no
        access. Authorization Code with S256 PKCE and explicit Workspace consent
        remain required. HTTPS web callbacks use reviewed registration instead.
        Client names are unverified. Optional extension metadata is ignored.
        This endpoint is advertised by OAuth authorization-server discovery.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - redirect_uris
                - token_endpoint_auth_method
              properties:
                client_name:
                  type: string
                  maxLength: 88
                redirect_uris:
                  type: array
                  minItems: 1
                  maxItems: 20
                  uniqueItems: true
                  items:
                    type: string
                    format: uri
                    maxLength: 2048
                token_endpoint_auth_method:
                  type: string
                  enum:
                    - none
                application_type:
                  type: string
                  enum:
                    - native
                grant_types:
                  type: array
                  maxItems: 2
                  uniqueItems: true
                  items:
                    type: string
                    enum:
                      - authorization_code
                      - refresh_token
                response_types:
                  type: array
                  maxItems: 1
                  items:
                    type: string
                    enum:
                      - code
                scope:
                  type: string
                  description: Space-separated supported MCP scopes, including additive assign:resource:read or assign:resource:write groups. Legacy broad scopes remain supported; grants and refresh never expand the approved scope set.
      responses:
        "201":
          description: Public client registered; no secret or token issued.
          content:
            application/json:
              schema:
                type: object
                required:
                  - client_id
                  - client_name
                  - redirect_uris
                  - token_endpoint_auth_method
                  - application_type
                  - grant_types
                  - response_types
                  - scope
                properties:
                  client_id:
                    type: string
                  client_name:
                    type: string
                  redirect_uris:
                    type: array
                    items:
                      type: string
                      format: uri
                  token_endpoint_auth_method:
                    type: string
                    enum:
                      - none
                  application_type:
                    type: string
                    enum:
                      - native
                  grant_types:
                    type: array
                    items:
                      type: string
                      enum:
                        - authorization_code
                        - refresh_token
                  response_types:
                    type: array
                    items:
                      type: string
                      enum:
                        - code
                  scope:
                    type: string
        "400":
          description: Invalid client metadata.
          content:
            application/json:
              schema: &a8
                type: object
                required:
                  - error
                  - error_description
                properties:
                  error:
                    type: string
                  error_description:
                    type: string
        "429":
          description: Registration rate limit exceeded; respect Retry-After.
        "500":
          description: Registration could not be persisted.
          content:
            application/json:
              schema: *a8
        "503":
          description: Registration capacity or deadline reached; respect Retry-After.
          headers:
            Retry-After:
              description: Seconds to wait before another registration attempt.
              schema:
                type: integer
                minimum: 1
                example: 60
          content:
            application/json:
              schema: *a8
  /api/v1/workspaces/{workspace_id}/evidence-collections:
    post:
      tags:
        - Discuss
      operationId: createEvidenceCollection
      summary: Capture exact private evidence passages
      description: "Membership-private native work capability. Current Workspace and resource permissions are checked on every request. Responses use Cache-Control: no-store. Requires a browser session and CSRF. Reuse Idempotency-Key only for the same request. Before retrying an uncertain write, resolve its operation receipt. Evidence collections are private, partial investigations. Hydration is required before reuse; references do not authorize shared publication. Changed, unavailable and excluded sources return no text. No inference occurs."
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/EvidenceCollectionCreateRequest"
      responses:
        "200":
          description: Capture exact private evidence passages
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EvidenceCollection"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/workspaces/{workspace_id}/evidence-collections/exclusions:
    post:
      tags:
        - Discuss
      operationId: excludeEvidenceCollection
      summary: Replace the exclusion set in one investigation version
      description: "Membership-private native work capability. Current Workspace and resource permissions are checked on every request. Responses use Cache-Control: no-store. Requires a browser session and CSRF. Reuse Idempotency-Key only for the same request. Before retrying an uncertain write, resolve its operation receipt. Evidence collections are private, partial investigations. Hydration is required before reuse; references do not authorize shared publication. Changed, unavailable and excluded sources return no text. No inference occurs."
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/EvidenceCollectionExcludeRequest"
      responses:
        "200":
          description: Replace the exclusion set in one investigation version
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EvidenceCollection"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/workspaces/{workspace_id}/evidence-collections/{id}:
    get:
      tags:
        - Discuss
      operationId: getEvidenceCollection
      summary: Read an exact private evidence collection
      description: "Membership-private native work capability. Current Workspace and resource permissions are checked on every request. Responses use Cache-Control: no-store. Exact versions never refresh or replace selected targets implicitly. Evidence collections are private, partial investigations. Hydration is required before reuse; references do not authorize shared publication. Changed, unavailable and excluded sources return no text. No inference occurs."
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - name: id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/Identifier"
        - name: version
          in: query
          required: true
          schema:
            type: integer
            minimum: 1
      responses:
        "200":
          description: Read an exact private evidence collection
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EvidenceCollection"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/workspaces/{workspace_id}/evidence-collections/{id}/hydrate:
    get:
      tags:
        - Discuss
      operationId: hydrateEvidenceCollection
      summary: Read up to twenty currently authorized exact passages
      description: "Membership-private native work capability. Current Workspace and resource permissions are checked on every request. Responses use Cache-Control: no-store. Exact versions never refresh or replace selected targets implicitly. Evidence collections are private, partial investigations. Hydration is required before reuse; references do not authorize shared publication. Changed, unavailable and excluded sources return no text. No inference occurs."
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - name: id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/Identifier"
        - name: version
          in: query
          required: true
          schema:
            type: integer
            minimum: 1
        - name: offset
          in: query
          required: false
          schema:
            type: integer
            minimum: 0
            maximum: 31
            default: 0
        - name: limit
          in: query
          required: false
          description: Use a smaller page to consume exact passages within a bounded context.
          schema:
            type: integer
            minimum: 1
            maximum: 20
            default: 20
      responses:
        "200":
          description: Read up to twenty currently authorized exact passages
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EvidenceHydration"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/workspaces/{workspace_id}/task-result-sets:
    post:
      tags:
        - Tasks
      operationId: createTaskResultSet
      summary: Capture or explicitly refresh a checked Task result set
      description: "Membership-private native work capability. Current Workspace and resource permissions are checked on every request. Responses use Cache-Control: no-store. Requires a browser session and CSRF. Reuse Idempotency-Key only for the same request. Before retrying an uncertain write, resolve its operation receipt."
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TaskResultSetCreateRequest"
      responses:
        "200":
          description: Capture or explicitly refresh a checked Task result set
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TaskResultSet"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/workspaces/{workspace_id}/task-result-sets/{id}:
    get:
      tags:
        - Tasks
      operationId: getTaskResultSet
      summary: Read an exact Task result-set version
      description: "Membership-private native work capability. Current Workspace and resource permissions are checked on every request. Responses use Cache-Control: no-store. Exact versions never refresh or replace selected targets implicitly."
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - name: id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/Identifier"
        - name: version
          in: query
          required: true
          schema:
            type: integer
            minimum: 1
      responses:
        "200":
          description: Read an exact Task result-set version
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TaskResultSet"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/workspaces/{workspace_id}/task-selections:
    post:
      tags:
        - Tasks
      operationId: createTaskSelection
      summary: Select exact result-set Task identities
      description: "Membership-private native work capability. Current Workspace and resource permissions are checked on every request. Responses use Cache-Control: no-store. Requires a browser session and CSRF. Reuse Idempotency-Key only for the same request. Before retrying an uncertain write, resolve its operation receipt."
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TaskSelectionCreateRequest"
      responses:
        "200":
          description: Select exact result-set Task identities
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TaskSelection"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/workspaces/{workspace_id}/task-selections/{id}:
    get:
      tags:
        - Tasks
      operationId: getTaskSelection
      summary: Read a frozen Task selection
      description: "Membership-private native work capability. Current Workspace and resource permissions are checked on every request. Responses use Cache-Control: no-store. Exact versions never refresh or replace selected targets implicitly."
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - name: id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/Identifier"
      responses:
        "200":
          description: Read a frozen Task selection
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TaskSelection"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/workspaces/{workspace_id}/task-result-sets/{id}/hydrate:
    get:
      tags:
        - Tasks
      operationId: hydrateTaskResultSet
      summary: Hydrate up to 100 exact result-set targets with current compact Task previews
      description: "Membership-private native work capability. Current Workspace and resource permissions are checked on every request. Responses use Cache-Control: no-store. Exact versions never refresh or replace selected targets implicitly."
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - name: id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/Identifier"
        - name: version
          in: query
          required: true
          schema:
            type: integer
            minimum: 1
        - name: offset
          in: query
          required: false
          schema:
            type: integer
            minimum: 0
            maximum: 999
            default: 0
      responses:
        "200":
          description: Hydrate up to 100 exact result-set targets with current compact Task previews
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TaskSelectionHydration"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/workspaces/{workspace_id}/task-selections/{id}/hydrate:
    get:
      tags:
        - Tasks
      operationId: hydrateTaskSelection
      summary: Reauthorize up to100 selected Task identities
      description: "Membership-private native work capability. Current Workspace and resource permissions are checked on every request. Responses use Cache-Control: no-store. Exact versions never refresh or replace selected targets implicitly."
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - name: id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/Identifier"
        - name: offset
          in: query
          required: false
          schema:
            type: integer
            minimum: 0
            maximum: 999
            default: 0
      responses:
        "200":
          description: Reauthorize up to100 selected Task identities
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TaskSelectionHydration"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/workspaces/{workspace_id}/task-saved-views:
    post:
      tags:
        - Tasks
      operationId: saveTaskSavedView
      summary: Create or revision-check a personal Task query
      description: "Membership-private native work capability. Current Workspace and resource permissions are checked on every request. Responses use Cache-Control: no-store. Requires a browser session and CSRF. Reuse Idempotency-Key only for the same request. Before retrying an uncertain write, resolve its operation receipt."
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TaskViewSaveRequest"
      responses:
        "200":
          description: Create or revision-check a personal Task query
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TaskSavedView"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
    get:
      tags:
        - Tasks
      operationId: listTaskSavedViews
      summary: List personal Task queries
      description: "Membership-private native work capability. Current Workspace and resource permissions are checked on every request. Responses use Cache-Control: no-store. Exact versions never refresh or replace selected targets implicitly."
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
      responses:
        "200":
          description: List personal Task queries
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TaskViewList"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/workspaces/{workspace_id}/task-saved-views/delete:
    post:
      tags:
        - Tasks
      operationId: deleteTaskSavedView
      summary: Delete an exact personal Task-query revision
      description: "Membership-private native work capability. Current Workspace and resource permissions are checked on every request. Responses use Cache-Control: no-store. Requires a browser session and CSRF. Reuse Idempotency-Key only for the same request. Before retrying an uncertain write, resolve its operation receipt."
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TaskViewDeleteRequest"
      responses:
        "200":
          description: Delete an exact personal Task-query revision
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CapabilityDeleted"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/workspaces/{workspace_id}/document-patches:
    post:
      tags:
        - Tasks
      operationId: createDocumentPatch
      summary: Create or edit a versioned Document draft
      description: "Membership-private native work capability. Current Workspace and resource permissions are checked on every request. Responses use Cache-Control: no-store. Requires a browser session and CSRF. Reuse Idempotency-Key only for the same request. Before retrying an uncertain write, resolve its operation receipt."
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/DocumentPatchCreateRequest"
      responses:
        "200":
          description: Create or edit a versioned Document draft
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DocumentPatchDraft"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/workspaces/{workspace_id}/document-patches/{id}:
    get:
      tags:
        - Tasks
      operationId: getDocumentPatch
      summary: Read an exact Document draft version
      description: "Membership-private native work capability. Current Workspace and resource permissions are checked on every request. Responses use Cache-Control: no-store. Exact versions never refresh or replace selected targets implicitly."
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - name: id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/Identifier"
        - name: version
          in: query
          required: true
          schema:
            type: integer
            minimum: 1
      responses:
        "200":
          description: Read an exact Document draft version
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DocumentPatchDraft"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/workspaces/{workspace_id}/change-sets:
    post:
      tags:
        - Tasks
      operationId: createChangeSet
      summary: Propose bounded versioned changes
      description: "Membership-private native work capability. Current Workspace and resource permissions are checked on every request. Responses use Cache-Control: no-store. Requires a browser session and CSRF. Reuse Idempotency-Key only for the same request. Before retrying an uncertain write, resolve its operation receipt."
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ChangeSetCreateRequest"
      responses:
        "200":
          description: Propose bounded versioned changes
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ChangeSet"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/workspaces/{workspace_id}/change-sets/{id}:
    get:
      tags:
        - Tasks
      operationId: getChangeSet
      summary: Read an exact proposal and accessible outcomes
      description: "Membership-private native work capability. Current Workspace and resource permissions are checked on every request. Responses use Cache-Control: no-store. Exact versions never refresh or replace selected targets implicitly."
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - name: id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/Identifier"
        - name: version
          in: query
          required: true
          schema:
            type: integer
            minimum: 1
      responses:
        "200":
          description: Read an exact proposal and accessible outcomes
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ChangeSet"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/workspaces/{workspace_id}/change-sets/apply:
    post:
      tags:
        - Tasks
      operationId: applyChangeSet
      summary: Apply an exact reviewed proposal
      description: "Membership-private native work capability. Current Workspace and resource permissions are checked on every request. Responses use Cache-Control: no-store. Requires a browser session and CSRF. Reuse Idempotency-Key only for the same request. Before retrying an uncertain write, resolve its operation receipt."
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ChangeSetApplyRequest"
      responses:
        "200":
          description: Apply an exact reviewed proposal
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ChangeSetResult"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/workspaces/{workspace_id}/change-sets/{id}/review:
    post:
      tags:
        - Tasks
      operationId: reviewChangeSet
      summary: Approve or reject an exact proposal as its initiating human
      description: "Membership-private native work capability. Current Workspace and resource permissions are checked on every request. Responses use Cache-Control: no-store. Requires a browser session and CSRF. Only the initiating human can review this exact digest; approval cannot be supplied through MCP."
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - name: id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/Identifier"
        - $ref: "#/components/parameters/CSRFToken"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ChangeSetReviewRequest"
      responses:
        "200":
          description: Approve or reject an exact proposal as its initiating human
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ChangeSet"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/workspaces/{workspace_id}/operation-receipts/{id}:
    get:
      tags:
        - Tasks
      operationId: getOperationReceipt
      summary: Resolve a committed operation under current authority
      description: "Membership-private native work capability. Current Workspace and resource permissions are checked on every request. Responses use Cache-Control: no-store. Exact versions never refresh or replace selected targets implicitly."
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - name: id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/Identifier"
      responses:
        "200":
          description: Resolve a committed operation under current authority
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OperationReceipt"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/workspaces/{workspace_id}/operation-receipts/undo:
    post:
      tags:
        - Tasks
      operationId: undoOperationReceipt
      summary: Conditionally compensate an initiating member operation
      description: "Membership-private native work capability. Current Workspace and resource permissions are checked on every request. Responses use Cache-Control: no-store. Requires a browser session and CSRF. Reuse Idempotency-Key only for the same request. Before retrying an uncertain write, resolve its operation receipt."
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/IdentityReference"
      responses:
        "200":
          description: Conditionally compensate an initiating member operation
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OperationReceipt"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/workspaces/{workspace_id}/members/resolve:
    get:
      tags:
        - Tasks
      operationId: resolveWorkspaceMember
      summary: Resolve an exact member identity with ambiguity preserved
      description: "Membership-private native work capability. Current Workspace and resource permissions are checked on every request. Responses use Cache-Control: no-store. Exact versions never refresh or replace selected targets implicitly."
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - name: query
          in: query
          required: true
          schema:
            type: string
            minLength: 1
            maxLength: 200
      responses:
        "200":
          description: Resolve an exact member identity with ambiguity preserved
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MemberResolution"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/workspaces/{workspace_id}/tasks/{id}/context:
    get:
      tags:
        - Tasks
      operationId: getTaskContext
      summary: Read bounded canonical Task context
      description: "Membership-private native work capability. Current Workspace and resource permissions are checked on every request. Responses use Cache-Control: no-store. Exact versions never refresh or replace selected targets implicitly."
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - name: id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/Identifier"
      responses:
        "200":
          description: Read bounded canonical Task context
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TaskContextPackage"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/workspaces/{workspace_id}/activity/groups:
    get:
      tags:
        - Activity
      operationId: listActivityGroups
      summary: Read authorized daily Activity groups
      description: Upcoming; not released. Additive grouped Activity companion. Existing event-shaped routes remain supported. Scope is required; resource_id is required for person/project/task and forbidden for workspace/account. Account scope requires a human account. Select one civil day or a paired inclusive from_day/exclusive before_day interval of at most 90 days. Omitted timezone uses the effective account/workspace setting; Local is invalid. Cursors bind every selector, current authority, policy and retained history. Supply originating selectors on expansion. Reads never grant access or replay effects. Current references and previews are observations, not historical event facts. Coverage before retained history is unknown.
      security:
        - browserSession: []
        - bearerToken: []
        - developerAccessToken: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - $ref: "#/components/parameters/WorkspaceRealtimeDerivedBaseline"
        - name: scope
          in: query
          required: true
          schema: &a9
            type: string
            enum:
              - workspace
              - account
              - person
              - project
              - task
        - name: resource_id
          in: query
          required: false
          schema: &a10
            type: string
            format: uuid
        - name: day
          in: query
          required: false
          schema: &a11
            type: string
            format: date
            maxLength: 10
        - name: from_day
          in: query
          required: false
          schema: &a12
            type: string
            format: date
            maxLength: 10
        - name: before_day
          in: query
          required: false
          schema: &a13
            type: string
            format: date
            maxLength: 10
        - name: time_zone
          in: query
          required: false
          schema: &a14
            type: string
            maxLength: 128
        - name: actor_ids
          in: query
          required: false
          schema: &a15
            type: array
            maxItems: 50
            items:
              type: string
              format: uuid
          style: form
          explode: false
        - name: project_ids
          in: query
          required: false
          schema: &a16
            type: array
            maxItems: 50
            items:
              type: string
              format: uuid
          style: form
          explode: false
        - name: activity_types
          in: query
          required: false
          schema: &a17
            type: array
            maxItems: 50
            items:
              type: string
              enum:
                - tasks
                - comments
                - documents
                - projects
                - milestones
                - development
                - files
                - members
          style: form
          explode: false
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 50
            default: 10
        - name: cursor
          in: query
          required: false
          schema: &a18
            type: string
            maxLength: 4096
        - name: include_previews
          in: query
          required: false
          schema: &a19
            type: boolean
            default: true
      responses:
        "200":
          description: Authorized Activity page; at most 64 KiB. All counts include explicit accuracy and retained-history coverage.
          headers:
            X-Assign-Realtime-Cursor:
              $ref: "#/components/headers/RealtimeReadCursor"
            X-Assign-Realtime-Position:
              $ref: "#/components/headers/RealtimeReadPosition"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ActivityGroupPage"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          description: "activity_cursor_stale: restart with the same selectors; activity_group_stale: refresh the parent before expansion."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "503":
          description: "activity_read_budget_exceeded: the bounded read exceeded its budget. Retry manually. Maintenance may also make the route unavailable."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /api/v1/workspaces/{workspace_id}/activity/groups/{group_id}/children:
    get:
      tags:
        - Activity
      operationId: listActivityGroupChildren
      summary: Expand an authorized Activity group
      description: Upcoming; not released. Additive grouped Activity companion. Existing event-shaped routes remain supported. Scope is required; resource_id is required for person/project/task and forbidden for workspace/account. Account scope requires a human account. Select one civil day or a paired inclusive from_day/exclusive before_day interval of at most 90 days. Omitted timezone uses the effective account/workspace setting; Local is invalid. Cursors bind every selector, current authority, policy and retained history. Supply originating selectors on expansion. Reads never grant access or replay effects. Current references and previews are observations, not historical event facts. Coverage before retained history is unknown.
      security:
        - browserSession: []
        - bearerToken: []
        - developerAccessToken: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - $ref: "#/components/parameters/WorkspaceRealtimeDerivedBaseline"
        - name: scope
          in: query
          required: true
          schema: *a9
        - name: resource_id
          in: query
          required: false
          schema: *a10
        - name: day
          in: query
          required: false
          schema: *a11
        - name: from_day
          in: query
          required: false
          schema: *a12
        - name: before_day
          in: query
          required: false
          schema: *a13
        - name: time_zone
          in: query
          required: false
          schema: *a14
        - name: actor_ids
          in: query
          required: false
          schema: *a15
          style: form
          explode: false
        - name: project_ids
          in: query
          required: false
          schema: *a16
          style: form
          explode: false
        - name: activity_types
          in: query
          required: false
          schema: *a17
          style: form
          explode: false
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 25
            default: 25
        - name: cursor
          in: query
          required: false
          schema: *a18
        - name: include_previews
          in: query
          required: false
          schema: *a19
        - name: group_id
          in: path
          required: true
          schema:
            type: string
            maxLength: 192
        - name: group_revision
          in: query
          required: true
          schema:
            type: string
            maxLength: 128
        - name: snapshot
          in: query
          required: true
          schema:
            type: string
            maxLength: 4096
      responses:
        "200":
          description: Authorized Activity page; at most 64 KiB. All counts include explicit accuracy and retained-history coverage.
          headers:
            X-Assign-Realtime-Cursor:
              $ref: "#/components/headers/RealtimeReadCursor"
            X-Assign-Realtime-Position:
              $ref: "#/components/headers/RealtimeReadPosition"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ActivityGroupChildPage"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          description: "activity_cursor_stale: restart with the same selectors; activity_group_stale: refresh the parent before expansion."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "503":
          description: "activity_read_budget_exceeded: the bounded read exceeded its budget. Retry manually. Maintenance may also make the route unavailable."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /api/v1/workspaces/{workspace_id}/activity:
    get:
      tags:
        - Workspaces
      operationId: listWorkspaceActivity
      summary: List access-filtered Workspace activity
      description: |
        Returns a newest-first, cursor-bounded page of meaningful work changes.
        Activity is projected from immutable domain events at read time using
        the caller's current access. Only registered activity types appear:
        progress, ownership, deadline, priority, milestone, description,
        blocking-dependency, comment, document, Project and Milestone changes.
        Personal preferences (following or muting), ordinary reactions,
        ordinary title corrections, configuration, security, session,
        identity, membership, integration, and Agent execution events never
        appear here; an ordinary edit remains available in that item's own
        history. `action` is a stable localization key, not a rendered
        sentence, and `summary` never includes comment, Document, or other
        free-form content. Events from one command or one editing session
        collapse into a single item: `count` says how many events it
        represents and `affected` lists up to 25 of the resources a bulk
        change touched. Private Project resources are filtered before
        pagination and grouping, so items, cursors, `has_more`, and
        `affected` cannot disclose inaccessible work. `limit` bounds the
        events examined, so a page may hold fewer items than `limit`; only
        `has_more` says whether history continues. `provenance` distinguishes
        mediated actions while preserving the stable Actor; a group carries
        the least direct classification among its members.
        Optional paired `day` and `time_zone` select one civil day's inclusive
        local midnight to exclusive next midnight, including DST changes.
        The indexed time range applies before pagination and counting; cursors
        are bound to that interval. Existing 90-day retention still applies.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/PageLimit"
        - name: day
          in: query
          description: Civil date; requires time_zone. Invalid or unpaired inputs return 400 invalid_request.
          schema:
            type: string
            format: date
        - name: time_zone
          in: query
          description: IANA timezone for day; requires day. Local is not accepted.
          schema:
            type: string
            example: Europe/Budapest
      responses:
        "200":
          description: Activity page
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ActivityList"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/tasks/{task_id}/activity:
    get:
      tags:
        - Tasks
      operationId: listTaskActivity
      summary: List access-filtered Task activity
      description: |
        Returns the Task's own history plus associated comment and Task
        relation events, newest first. It shares the Workspace Activity
        retention, redaction, current-access filtering, grouping, and safe
        projection rules, and it additionally includes ordinary property
        changes (such as a title correction) that the Workspace feed omits.
        Personal following and muting changes and ordinary reactions are not
        included. Private-Project access is re-evaluated for every request.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/TaskId"
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/PageLimit"
      responses:
        "200":
          description: Activity page
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ActivityList"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/projects/{project_id}/activity:
    get:
      tags:
        - Projects
      operationId: listProjectActivity
      summary: List access-filtered Project activity
      description: |
        Returns newest-first, cursor-bounded meaningful work changes inside one
        Project: its own changes plus its Tasks, comments, Documents and
        Milestones. It shares the Workspace Activity retention, redaction,
        grouping, and safe projection rules, and Project read access is
        re-evaluated for every request, so a private Project the caller cannot
        read returns `404` and no item, cursor, or `has_more` value can disclose
        inaccessible work. `limit` bounds the events examined, so a page may
        hold fewer items than `limit`; only `has_more` says whether history
        continues. Activity is immutable historical evidence and never
        determines which Tasks are in progress.
      security:
        - browserSession: []
        - nativeAccessToken: []
      parameters:
        - $ref: "#/components/parameters/ProjectId"
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/PageLimit"
      responses:
        "200":
          description: Activity page
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ActivityList"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/workspaces/{workspace_id}/people/{username}:
    get:
      tags:
        - People
      operationId: getWorkspacePersonProfile
      summary: Get a workspace-scoped person profile
      description: |
        Returns the active Workspace membership identity visible to the
        caller. The canonical target is the User's current username. A
        historical username resolves to the same User; a UUIDv7 is accepted
        only as a temporary migration reference. This is a collaboration
        profile, not account settings: it
        carries the title, bio and status the person set in Account Profile
        settings, and never exposes email, phone, or other private account
        data. The returned `actor_id`
        can be used with the normal Workspace Task list's
        `assignee_actor_id` filter.
      security:
        - browserSession: []
        - nativeAccessToken: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - $ref: "#/components/parameters/PersonUsername"
      responses:
        "200":
          description: Workspace-visible person profile
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WorkspacePersonProfile"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/workspaces/{workspace_id}/people/{user_id}/profile-picture/content:
    get:
      tags:
        - People
      operationId: getWorkspacePersonProfilePicture
      summary: Read a workspace member's uploaded profile picture
      description: Redirects an authorized Workspace member to a short-lived object-storage URL. The target must still be an active member of the same Workspace.
      security:
        - browserSession: []
        - nativeAccessToken: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - name: user_id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/Identifier"
      responses:
        "200":
          description: Image bytes after a client follows the redirect.
        "302":
          description: Short-lived object-storage redirect.
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
  /api/v1/workspaces/{workspace_id}/people/{username}/activity:
    get:
      tags:
        - People
      operationId: listWorkspacePersonActivity
      summary: List a person's visible workspace activity
      description: |
        Returns the requested active member's human-readable activity,
        newest first. It uses the same redaction and current-access rules as
        Workspace Activity and never returns raw audit events.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - $ref: "#/components/parameters/PersonUsername"
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/PageLimit"
      responses:
        "200":
          description: Person activity page
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ActivityList"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/workspaces/{workspace_id}/inbox:
    get:
      tags:
        - Account
      operationId: listInboxItems
      summary: List the current user's Inbox for a Workspace
      description: Generated Discuss message notices are excluded from items and unread totals; read them in Discuss.
      security:
        - browserSession: []
        - nativeAccessToken: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - $ref: "#/components/parameters/PageLimit"
        - name: cursor
          in: query
          schema:
            $ref: "#/components/schemas/Cursor"
        - name: unread_only
          in: query
          schema:
            type: boolean
            default: false
        - name: category
          in: query
          schema:
            $ref: "#/components/schemas/NotificationCategory"
          description: Optional typed category filter. Pagination cursors are bound to this filter.
        - name: X-Assign-Realtime-Baseline
          in: header
          required: false
          description: Upcoming browser-session canonical read custody. The named private scope captures this response and a signed replay cursor in one snapshot. Successful responses include X-Assign-Realtime-Cursor and X-Assign-Realtime-Position. Apply all required retained state before acknowledging the corresponding socket checkpoint. Failed or incomplete baselines return no cursor.
          schema:
            type: string
            enum:
              - inbox
      responses:
        "200":
          description: Inbox items and the current unread count
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/InboxList"
          headers:
            X-Assign-Realtime-Cursor:
              description: Signed scope/User/session-bound replay custody; only when the requested canonical baseline succeeds.
              schema:
                type: string
                maxLength: 4096
            X-Assign-Realtime-Position:
              description: Decimal-string snapshot position; only with a successful baseline cursor.
              schema:
                type: string
                pattern: ^(0|[1-9][0-9]*)$
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/inbox-items/{inbox_item_id}:
    patch:
      tags:
        - Account
      operationId: updateInboxItemReadState
      summary: Mark one Inbox item read or unread
      description: |
        Generated Discuss message notices are not Inbox items and return the existing not-found response.
        Browser callers must send the session-bound `X-CSRF-Token`; native
        callers authenticate with a native access token and must not send a
        browser cookie or CSRF value. Both credential types operate only on
        the caller's own Inbox item.
      security:
        - browserSession: []
        - nativeAccessToken: []
      parameters:
        - name: inbox_item_id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/Identifier"
        - $ref: "#/components/parameters/BrowserCSRFToken"
        - $ref: "#/components/parameters/IfMatch"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/InboxReadStateRequest"
      responses:
        "200":
          description: Updated Inbox item
          headers:
            ETag:
              $ref: "#/components/headers/ETag"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/InboxItem"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/workspaces/{workspace_id}/settings:
    get:
      tags:
        - Workspaces
      operationId: getWorkspaceSettings
      summary: Read shared Workspace defaults
      security:
        - browserSession: []
        - nativeAccessToken: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
      responses:
        "200":
          description: Workspace settings.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WorkspaceSettings"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
    patch:
      tags:
        - Workspaces
      operationId: updateWorkspaceSettings
      summary: Update shared Workspace defaults
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/IfMatch"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/WorkspaceSettingsPatch"
      responses:
        "200":
          description: Updated Workspace settings.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WorkspaceSettings"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
  /api/v1/workspaces/{workspace_id}/knowledge-settings:
    get:
      tags:
        - Workspaces
      operationId: getWorkspaceKnowledgeSettings
      summary: Read Workspace Knowledge indexing settings and lifecycle state
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
      responses:
        "200":
          description: Knowledge settings.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/KnowledgeSettings"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
    patch:
      tags:
        - Workspaces
      operationId: updateWorkspaceKnowledgeSettings
      summary: Update desired Workspace Knowledge indexing settings
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/IfMatch"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/KnowledgeSettingsPatch"
      responses:
        "200":
          description: Updated desired state.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/KnowledgeSettings"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
  /api/v1/workspaces/{workspace_id}/knowledge-repository:
    get:
      tags:
        - Documents
      operationId: getKnowledgeRepository
      summary: Read the Workspace Git-backed Documents repository
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
      responses:
        "200":
          description: Current repository status.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/KnowledgeRepository"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
    put:
      tags:
        - Documents
      operationId: connectKnowledgeRepository
      summary: Validate and connect one existing documentation repository
      description: Requires explicit acknowledgement that every repository reader can read all mirrored Documents. The configured branch must already exist. No repository, branch, or pull request is created.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - $ref: "#/components/parameters/CSRFToken"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/KnowledgeRepositoryConnect"
      responses:
        "200":
          description: Validated connection.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/KnowledgeRepository"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "422":
          $ref: "#/components/responses/BadRequest"
    patch:
      tags:
        - Documents
      operationId: updateKnowledgeRepositoryState
      summary: Pause, resume, or disconnect Git-backed Documents
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - $ref: "#/components/parameters/CSRFToken"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/KnowledgeRepositoryStateUpdate"
      responses:
        "200":
          description: Updated connection state.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/KnowledgeRepository"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
  /api/v1/workspaces/{workspace_id}/knowledge-repository/import:
    post:
      tags:
        - Documents
      operationId: importKnowledgeRepository
      summary: Import bounded Markdown content from the connected repository
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - $ref: "#/components/parameters/CSRFToken"
      responses:
        "202":
          description: Completed import receipt.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/KnowledgeRepositorySyncRun"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
  /api/v1/workspaces/{workspace_id}/knowledge-repository/sync:
    post:
      tags:
        - Documents
      operationId: syncKnowledgeRepository
      summary: Synchronize pending Assign and Git changes now
      description: Bypasses the debounce only. Current permission, expected-head, conflict, size, and rate gates still apply.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - $ref: "#/components/parameters/CSRFToken"
      responses:
        "202":
          description: Synchronization receipt.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/KnowledgeRepositorySyncRun"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
  /api/v1/workspaces/{workspace_id}/knowledge-repository/runs:
    get:
      tags:
        - Documents
      operationId: listKnowledgeRepositorySyncRuns
      summary: List bounded repository synchronization history
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - $ref: "#/components/parameters/PageLimit"
      responses:
        "200":
          description: Synchronization history.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/KnowledgeRepositorySyncRunList"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
  /api/v1/workspaces/{workspace_id}/knowledge-repository/conflicts:
    get:
      tags:
        - Documents
      operationId: listKnowledgeRepositoryConflicts
      summary: List open revision-bound synchronization conflicts
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - $ref: "#/components/parameters/PageLimit"
      responses:
        "200":
          description: Open conflicts.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/KnowledgeRepositoryConflictList"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
  /api/v1/workspaces/{workspace_id}/knowledge-repository/conflicts/{conflict_id}/resolve:
    post:
      tags:
        - Documents
      operationId: resolveKnowledgeRepositoryConflict
      summary: Apply a revision-bound explicit conflict decision
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - name: conflict_id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/Identifier"
        - $ref: "#/components/parameters/CSRFToken"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/KnowledgeRepositoryConflictResolution"
      responses:
        "202":
          description: Conflict-resolution synchronization receipt.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/KnowledgeRepositorySyncRun"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
  /api/v1/workspaces/{workspace_id}/agents/definitions:
    get:
      tags:
        - Agents
      operationId: listWorkspaceAgentDefinitions
      summary: List curated Agent definitions available to a Workspace
      description: Returns customer-safe product metadata only. Confidential instructions, model/runtime configuration, internal skills, and raw tool policy are never returned.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
      responses:
        "200":
          description: Curated Agent definitions.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AgentDefinitionList"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/workspaces/{workspace_id}/agents/studio/definitions:
    get:
      tags:
        - Agents
      operationId: listWorkspaceCustomAgents
      summary: List custom Agent definitions editable by Workspace managers
      description: Returns updated-at ordered pages under current Studio authority. A continuation cursor binds the prior page's commit-ordered Workspace journal position. A catalog change returns 409 catalog_changed; restart from the first page.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - $ref: "#/components/parameters/PageLimit"
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/WorkspaceRealtimeBaseline"
      responses:
        "200":
          description: Cursor-paginated custom Agent definitions.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CustomAgentDefinitionList"
          headers:
            X-Assign-Realtime-Cursor:
              $ref: "#/components/headers/RealtimeReadCursor"
            X-Assign-Realtime-Position:
              $ref: "#/components/headers/RealtimeReadPosition"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
    post:
      tags:
        - Agents
      operationId: createWorkspaceCustomAgent
      summary: Save a bounded, versioned custom Agent definition
      description: Requires the distinct custom-Agent entitlement and Workspace management authority. Saving never executes the Agent.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - $ref: "#/components/parameters/CSRFToken"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CustomAgentDefinitionInput"
      responses:
        "201":
          description: Custom Agent definition saved.
          headers:
            ETag:
              $ref: "#/components/headers/ETag"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CustomAgentDefinition"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          description: The Workspace does not have an active custom-Agent entitlement.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "403":
          $ref: "#/components/responses/Forbidden"
        "409":
          $ref: "#/components/responses/Conflict"
  /api/v1/workspaces/{workspace_id}/agents/studio/definitions/{definition_id}:
    get:
      tags:
        - Agents
      operationId: getWorkspaceCustomAgent
      summary: Read the current editable custom Agent definition
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - name: definition_id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/Identifier"
        - $ref: "#/components/parameters/WorkspaceRealtimeBaseline"
      responses:
        "200":
          description: Current custom Agent definition.
          headers:
            ETag:
              $ref: "#/components/headers/ETag"
            X-Assign-Realtime-Cursor:
              $ref: "#/components/headers/RealtimeReadCursor"
            X-Assign-Realtime-Position:
              $ref: "#/components/headers/RealtimeReadPosition"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CustomAgentDefinition"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
    patch:
      tags:
        - Agents
      operationId: updateWorkspaceCustomAgent
      summary: Save a new immutable revision of a custom Agent definition
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - name: definition_id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/Identifier"
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/IfMatch"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CustomAgentDefinitionInput"
      responses:
        "200":
          description: Updated custom Agent definition.
          headers:
            ETag:
              $ref: "#/components/headers/ETag"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CustomAgentDefinition"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
  /api/v1/workspaces/{workspace_id}/agents/studio/definitions/{definition_id}/requests:
    get:
      tags:
        - Agents
      operationId: listWorkspaceCustomAgentRequests
      summary: List permission-filtered custom Agent request outcomes
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - name: definition_id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/Identifier"
        - $ref: "#/components/parameters/PageLimit"
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/WorkspaceRealtimeBaseline"
      responses:
        "200":
          description: Cursor-paginated visible request outcomes.
          headers:
            X-Assign-Realtime-Cursor:
              $ref: "#/components/headers/RealtimeReadCursor"
            X-Assign-Realtime-Position:
              $ref: "#/components/headers/RealtimeReadPosition"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AgentParticipationRequestList"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          description: Custom-Agent execution is not entitled.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "403":
          $ref: "#/components/responses/Forbidden"
    post:
      tags:
        - Agents
      operationId: admitWorkspaceCustomAgentRequest
      summary: Durably admit a manual or preview-only test request
      description: Test is the only preview-only source kind. Each request pins the current definition revision and charges actual settled compute.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - name: definition_id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/Identifier"
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AgentParticipationRequestInput"
      responses:
        "202":
          description: Request durably queued.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AgentParticipationRequest"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          description: Custom-Agent execution is not entitled.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
  /api/v1/workspaces/{workspace_id}/agents/studio/definitions/{definition_id}/profile:
    get:
      tags:
        - Agents
      operationId: getWorkspaceCustomAgentProfile
      summary: Read a member-visible custom Agent profile
      description: Returns stable identity and current permission-filtered run facts without definition instructions, binding identifiers, private input, or provider traces.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - name: definition_id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/Identifier"
        - $ref: "#/components/parameters/WorkspaceRealtimeBaseline"
      responses:
        "200":
          description: Safe custom Agent profile.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CustomAgentProfile"
          headers:
            X-Assign-Realtime-Cursor:
              $ref: "#/components/headers/RealtimeReadCursor"
            X-Assign-Realtime-Position:
              $ref: "#/components/headers/RealtimeReadPosition"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          description: Custom-Agent execution is not entitled.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/workspaces/{workspace_id}/agents/studio/definitions/{definition_id}/delegations/{task_id}:
    post:
      tags:
        - Agents
      operationId: delegateTaskToWorkspaceCustomAgent
      summary: Add a revision-fenced Agent responsibility overlay to a Task
      description: Preserves the primary human assignment and Task lifecycle. The request body must use source_kind delegation and the same task_id.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - name: definition_id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/Identifier"
        - name: task_id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/Identifier"
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AgentParticipationRequestInput"
      responses:
        "202":
          description: Delegation and request recorded atomically.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AgentParticipationRequest"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          description: Custom-Agent execution is not entitled.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
  /api/v1/workspaces/{workspace_id}/agents/studio/definitions/{definition_id}/usage:
    get:
      tags:
        - Agents
      operationId: getWorkspaceCustomAgentUsage
      summary: Read settled and pending credit usage for one custom Agent and period
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - name: definition_id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/Identifier"
        - name: from
          in: query
          schema:
            $ref: "#/components/schemas/Timestamp"
          description: Inclusive period start; defaults to one month before the request.
        - name: to
          in: query
          schema:
            $ref: "#/components/schemas/Timestamp"
          description: Exclusive period end; defaults to request time.
        - $ref: "#/components/parameters/WorkspaceRealtimeBaseline"
      responses:
        "200":
          description: Reconciled usage aggregate.
          headers:
            X-Assign-Realtime-Cursor:
              $ref: "#/components/headers/RealtimeReadCursor"
            X-Assign-Realtime-Position:
              $ref: "#/components/headers/RealtimeReadPosition"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CustomAgentUsage"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          description: Custom-Agent execution is not entitled.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "403":
          $ref: "#/components/responses/Forbidden"
  /api/v1/workspaces/{workspace_id}/agents:
    get:
      tags:
        - Agents
      operationId: listWorkspaceAgents
      summary: List Workspace-owned Agent instances
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - $ref: "#/components/parameters/WorkspaceRealtimeBaseline"
      responses:
        "200":
          description: Workspace Agent instances.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AgentInstanceList"
          headers:
            X-Assign-Realtime-Cursor:
              $ref: "#/components/headers/RealtimeReadCursor"
            X-Assign-Realtime-Position:
              $ref: "#/components/headers/RealtimeReadPosition"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
    post:
      tags:
        - Agents
      operationId: createWorkspaceAgent
      summary: Hire one curated Agent into a Workspace
      description: Creates Core-owned desired state. It does not synchronously invoke the Knowledge runtime or imply that a run succeeded.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AgentInstanceCreateRequest"
      responses:
        "201":
          description: Agent hired.
          headers:
            ETag:
              $ref: "#/components/headers/ETag"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AgentInstance"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/workspaces/{workspace_id}/agents/{agent_id}:
    get:
      tags:
        - Agents
      operationId: getWorkspaceAgent
      summary: Read one Workspace Agent instance
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - name: agent_id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/Identifier"
        - $ref: "#/components/parameters/WorkspaceRealtimeBaseline"
      responses:
        "200":
          description: Workspace Agent instance.
          headers:
            ETag:
              $ref: "#/components/headers/ETag"
            X-Assign-Realtime-Cursor:
              $ref: "#/components/headers/RealtimeReadCursor"
            X-Assign-Realtime-Position:
              $ref: "#/components/headers/RealtimeReadPosition"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AgentInstance"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
    patch:
      tags:
        - Agents
      operationId: updateWorkspaceAgent
      summary: Update one Workspace Agent's bounded configuration
      description: Updates scope, supported trigger selection, additional instructions, or active/paused state. Definition, model, provider, prompt, skill, MCP, and raw capability controls are not accepted.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - name: agent_id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/Identifier"
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/IfMatch"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AgentConfiguration"
      responses:
        "200":
          description: Updated Agent configuration.
          headers:
            ETag:
              $ref: "#/components/headers/ETag"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AgentInstance"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
    delete:
      tags:
        - Agents
      operationId: removeWorkspaceAgent
      summary: Soft-remove one Agent and stop all unfinished work
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - name: agent_id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/Identifier"
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/IfMatch"
      responses:
        "200":
          description: Removed Agent tombstone.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AgentInstance"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
  /api/v1/workspaces/{workspace_id}/agents/{agent_id}/quarantine:
    post:
      tags:
        - Agents
      operationId: quarantineWorkspaceAgent
      summary: Quarantine an Agent, cancel unfinished runs, and invalidate pending approvals
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - name: agent_id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/Identifier"
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/IfMatch"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AgentQuarantineRequest"
      responses:
        "200":
          description: Quarantined Agent.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AgentInstance"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "409":
          $ref: "#/components/responses/Conflict"
  /api/v1/workspaces/{workspace_id}/agents/{agent_id}/rollback:
    post:
      tags:
        - Agents
      operationId: rollbackWorkspaceAgent
      summary: Restore the prior qualified definition version in paused state
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - name: agent_id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/Identifier"
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/IfMatch"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AgentRunCancelRequest"
      responses:
        "200":
          description: Rolled-back paused Agent.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AgentInstance"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "409":
          $ref: "#/components/responses/Conflict"
  /api/v1/workspaces/{workspace_id}/agents/{agent_id}/runs:
    get:
      tags:
        - Agents
      operationId: listWorkspaceAgentRuns
      summary: List retained safe run history for one Agent
      description: Returns only Core-owned safe projections. Prompts, retrieved context, raw model output, chain of thought, provider details, and private runtime traces are never returned.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - name: agent_id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/Identifier"
        - $ref: "#/components/parameters/PageLimit"
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/WorkspaceRealtimeBaseline"
      responses:
        "200":
          description: Cursor-paginated Agent run history.
          headers:
            X-Assign-Realtime-Cursor:
              $ref: "#/components/headers/RealtimeReadCursor"
            X-Assign-Realtime-Position:
              $ref: "#/components/headers/RealtimeReadPosition"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AgentRunList"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
    post:
      tags:
        - Agents
      operationId: createWorkspaceAgentRun
      summary: Durably request a manual Agent run
      description: Creates the Core-owned queued run before returning. Admission requires an active, reconciled Agent with manual execution enabled; execution remains asynchronous.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - name: agent_id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/Identifier"
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AgentRunCreateRequest"
      responses:
        "202":
          description: Manual run durably queued.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AgentRun"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/workspaces/{workspace_id}/agents/{agent_id}/runs/{run_id}:
    get:
      tags:
        - Agents
      operationId: getWorkspaceAgentRun
      summary: Read one safe Agent run projection
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - name: agent_id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/Identifier"
        - name: run_id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/Identifier"
        - $ref: "#/components/parameters/WorkspaceRealtimeBaseline"
      responses:
        "200":
          description: Safe Agent run projection.
          headers:
            X-Assign-Realtime-Cursor:
              $ref: "#/components/headers/RealtimeReadCursor"
            X-Assign-Realtime-Position:
              $ref: "#/components/headers/RealtimeReadPosition"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AgentRun"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/workspaces/{workspace_id}/agents/{agent_id}/runs/{run_id}/cancel:
    post:
      tags:
        - Agents
      operationId: cancelWorkspaceAgentRun
      summary: Request cancellation of a non-terminal Agent run
      description: The initiating actor or an Agent manager may cancel. Already terminal runs return a conflict and committed actions remain intact.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - name: agent_id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/Identifier"
        - name: run_id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/Identifier"
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AgentRunCancelRequest"
      responses:
        "200":
          description: Cancellation recorded.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AgentRun"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/workspaces/{workspace_id}/agents/{agent_id}/runs/{run_id}/actions:
    get:
      tags:
        - Agents
      operationId: listWorkspaceAgentRunActions
      summary: List the run's attributable action and exact approval records
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - name: agent_id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/Identifier"
        - name: run_id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/Identifier"
        - $ref: "#/components/parameters/WorkspaceRealtimeBaseline"
      responses:
        "200":
          description: Action records.
          headers:
            X-Assign-Realtime-Cursor:
              $ref: "#/components/headers/RealtimeReadCursor"
            X-Assign-Realtime-Position:
              $ref: "#/components/headers/RealtimeReadPosition"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AgentActionList"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
  /api/v1/workspaces/{workspace_id}/agents/{agent_id}/runs/{run_id}/outputs:
    get:
      tags:
        - Agents
      operationId: listWorkspaceAgentRunOutputs
      summary: List customer-safe comments, reports, and notifications with full Agent provenance
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - name: agent_id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/Identifier"
        - name: run_id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/Identifier"
        - $ref: "#/components/parameters/WorkspaceRealtimeBaseline"
      responses:
        "200":
          description: Provenance-bound outputs.
          headers:
            X-Assign-Realtime-Cursor:
              $ref: "#/components/headers/RealtimeReadCursor"
            X-Assign-Realtime-Position:
              $ref: "#/components/headers/RealtimeReadPosition"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AgentOutputList"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
  /api/v1/workspaces/{workspace_id}/agents/{agent_id}/runs/{run_id}/actions/{action_id}/approvals/{approval_id}/decision:
    post:
      tags:
        - Agents
      operationId: decideWorkspaceAgentApproval
      summary: Approve or reject the exact immutable Agent action proposal once
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - name: agent_id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/Identifier"
        - name: run_id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/Identifier"
        - name: action_id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/Identifier"
        - name: approval_id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/Identifier"
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AgentApprovalDecision"
      responses:
        "200":
          description: Decided approval and resulting action state.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AgentAction"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
  /api/v1/workspaces/{workspace_id}/knowledge/search:
    get:
      tags:
        - Knowledge
      operationId: searchWorkspaceKnowledge
      summary: Search Workspace Knowledge for evidence
      description: Runs one explicitly submitted, bounded, permission-filtered Knowledge query. This operation is distinct from ordinary lexical Search and represents disabled, unready, and temporarily unavailable Knowledge without blocking canonical Workspace work.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - $ref: "#/components/parameters/WorkspaceRealtimeDerivedBaseline"
        - name: q
          in: query
          required: true
          schema:
            type: string
            minLength: 2
            maxLength: 200
        - name: project_id
          in: query
          description: Strictly restrict evidence to one currently authorized included Project plus permitted shared material.
          schema:
            $ref: "#/components/schemas/Identifier"
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 20
            default: 20
      responses:
        "200":
          description: Bounded evidence projection or an explicit empty/unavailable state.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
            X-Assign-Realtime-Cursor:
              $ref: "#/components/headers/RealtimeReadCursor"
            X-Assign-Realtime-Position:
              $ref: "#/components/headers/RealtimeReadPosition"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/KnowledgeSearchProjection"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/workspaces/{workspace_id}/knowledge/answer:
    get:
      tags:
        - Knowledge
      operationId: answerWorkspaceKnowledge
      summary: Answer a submitted question from bounded Workspace evidence
      description: Uses the same permission-filtered retrieval capability as Discuss and MCP. The answer is explicitly submitted, extractive, source-bounded, and may abstain; typing never invokes Knowledge.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - $ref: "#/components/parameters/WorkspaceRealtimeDerivedBaseline"
        - name: q
          in: query
          required: true
          schema:
            type: string
            minLength: 2
            maxLength: 500
      responses:
        "200":
          description: Bounded answer with at most five permission-filtered sources.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
            X-Assign-Realtime-Cursor:
              $ref: "#/components/headers/RealtimeReadCursor"
            X-Assign-Realtime-Position:
              $ref: "#/components/headers/RealtimeReadPosition"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/KnowledgeAnswer"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/workspaces/{workspace_id}/discuss/availability:
    get:
      tags:
        - Discuss
      operationId: getDiscussAvailability
      summary: Resolve the current membership's Discuss access state
      description: Returns a server-owned feature decision before the client loads private history. Plan labels are presentation metadata; clients must branch on access and reason.
      security:
        - browserSession: []
        - bearerToken: []
        - developerAccessToken: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - $ref: "#/components/parameters/WorkspaceRealtimeBaseline"
      responses:
        "200":
          description: Current Workspace and membership-specific Discuss availability.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
            X-Assign-Realtime-Cursor:
              $ref: "#/components/headers/RealtimeReadCursor"
            X-Assign-Realtime-Position:
              $ref: "#/components/headers/RealtimeReadPosition"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DiscussAvailability"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/workspaces/{workspace_id}/discuss/messages:
    get:
      tags:
        - Discuss
      operationId: listDiscussMessages
      summary: List the current membership's private Discuss messages
      security:
        - browserSession: []
        - bearerToken: []
        - developerAccessToken: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - name: before
          in: query
          schema:
            type: integer
            format: int64
            minimum: 1
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 50
      responses:
        "200":
          description: Chronological bounded page.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DiscussMessagePage"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
    post:
      tags:
        - Discuss
      operationId: sendDiscussMessage
      summary: Queue one private message for the Discuss supervisor
      description: Persists the user message and pending assistant turn, then returns without waiting for inference. The same supervisor runtime, native MCP tools and streamed event projection used by POST /discuss/turns complete the turn. Reuse the same Idempotency-Key to retrieve the existing turn without starting another run.
      security:
        - browserSession: []
        - bearerToken: []
        - developerAccessToken: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/DiscussSendRequest"
      responses:
        "201":
          description: Persisted user message and queued assistant turn.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DiscussTurn"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          description: Discuss is not entitled or Workspace AI credits are exhausted.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/workspaces/{workspace_id}/discuss/references/resolve:
    post:
      tags:
        - Discuss
      operationId: resolveDiscussReference
      summary: Resolve a native reference with current Workspace permissions
      security:
        - browserSession: []
        - bearerToken: []
        - developerAccessToken: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - $ref: "#/components/parameters/CSRFToken"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/DiscussNativeReference"
      responses:
        "200":
          description: Private stream operation; shared Workspace storage accounting applies.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DiscussResolvedReference"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/workspaces/{workspace_id}/discuss/uploads:
    post:
      tags:
        - Discuss
      operationId: reserveDiscussUpload
      summary: Reserve a private file in Workspace storage
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - $ref: "#/components/parameters/CSRFToken"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/DiscussUploadRequest"
      responses:
        "201":
          description: Private stream operation; shared Workspace storage accounting applies.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AttachmentUpload"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          description: Discuss is not entitled or Workspace AI credits are exhausted.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/workspaces/{workspace_id}/discuss/uploads/{upload_id}/complete:
    post:
      tags:
        - Discuss
      operationId: completeDiscussUpload
      summary: Verify and complete a private upload
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - $ref: "#/components/parameters/CSRFToken"
        - name: upload_id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/Identifier"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AttachmentUploadCompleteRequest"
      responses:
        "200":
          description: Private stream operation; shared Workspace storage accounting applies.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DiscussFile"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          description: Discuss is not entitled or Workspace AI credits are exhausted.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/workspaces/{workspace_id}/discuss/uploads/{upload_id}:
    delete:
      tags:
        - Discuss
      operationId: cancelDiscussUpload
      summary: Cancel a private upload reservation
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - $ref: "#/components/parameters/CSRFToken"
        - name: upload_id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/Identifier"
      responses:
        "204":
          description: Private stream operation; shared Workspace storage accounting applies.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/workspaces/{workspace_id}/discuss/files/{file_id}:
    get:
      tags:
        - Discuss
      operationId: getDiscussFile
      summary: Read private file metadata
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - name: file_id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/Identifier"
      responses:
        "200":
          description: Private stream operation; shared Workspace storage accounting applies.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DiscussFile"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
    delete:
      tags:
        - Discuss
      operationId: deleteDiscussFile
      summary: Delete a private file and release its storage
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - $ref: "#/components/parameters/CSRFToken"
        - name: file_id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/Identifier"
      responses:
        "204":
          description: Private stream operation; shared Workspace storage accounting applies.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/workspaces/{workspace_id}/discuss/files/{file_id}/download:
    post:
      tags:
        - Discuss
      operationId: authorizeDiscussFileDownload
      summary: Authorize a private file download
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - $ref: "#/components/parameters/CSRFToken"
        - name: file_id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/Identifier"
      responses:
        "200":
          description: Private stream operation; shared Workspace storage accounting applies.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DiscussFileURL"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/workspaces/{workspace_id}/discuss/files/{file_id}/preview:
    post:
      tags:
        - Discuss
      operationId: authorizeDiscussFilePreview
      summary: Authorize a private file preview
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - $ref: "#/components/parameters/CSRFToken"
        - name: file_id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/Identifier"
      responses:
        "200":
          description: Private stream operation; shared Workspace storage accounting applies.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DiscussFileURL"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/workspaces/{workspace_id}/discuss/turns:
    post:
      tags:
        - Discuss
      operationId: enqueueDiscussTurn
      summary: Persist a private turn for server-owned generation
      description: Admits the turn immediately and orders its response behind any active foreground run. Standalone greetings receive a source-independent reply after authorization. Greetings containing a substantive question or resource references use normal evidence retrieval.
      security:
        - browserSession: []
        - bearerToken: []
        - developerAccessToken: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/DiscussEnqueueRequest"
      responses:
        "202":
          description: Private canonical Discuss state.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DiscussTurn"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          description: Discuss is not entitled or Workspace AI credits are exhausted.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/workspaces/{workspace_id}/discuss/voice/sessions:
    post:
      tags:
        - Discuss
      operationId: createDiscussLiveSession
      summary: Create a private GPT Live WebRTC session
      description: |
        Exchanges a browser WebRTC offer for a server-configured GPT Live answer.
        The OpenAI API key remains on the private Knowledge service. Assign selects
        the model, disables provider storage, supplies bounded private Discuss
        history, and configures client delegation back into canonical Discuss.
        The client must wait for session.started before treating voice as ready.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - $ref: "#/components/parameters/CSRFToken"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/DiscussLiveSessionRequest"
      responses:
        "201":
          description: Non-stored GPT Live WebRTC session transport.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DiscussLiveSession"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          description: Discuss is not entitled.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/workspaces/{workspace_id}/discuss/events:
    get:
      tags:
        - Discuss
      operationId: listDiscussEvents
      summary: Replay up to 100 ordered private message changes
      security:
        - browserSession: []
        - bearerToken: []
        - developerAccessToken: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - name: after
          in: query
          schema:
            type: integer
            format: int64
            minimum: 0
            default: 0
      responses:
        "200":
          description: Private canonical Discuss state.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DiscussEventPage"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/workspaces/{workspace_id}/discuss/messages/{message_id}:
    get:
      tags:
        - Discuss
      operationId: getDiscussMessage
      summary: Resolve one exact private message
      security:
        - browserSession: []
        - bearerToken: []
        - developerAccessToken: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - name: message_id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/Identifier"
      responses:
        "200":
          description: Private canonical Discuss state.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DiscussMessage"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/workspaces/{workspace_id}/discuss/messages/{message_id}/run-events:
    get:
      tags:
        - Discuss
      operationId: listDiscussRunEvents
      summary: List safe activity for one private Discuss response
      description: Returns a bounded run-local sequence containing only user-visible lifecycle events and sanitized tool activity. Raw tool arguments/results, prompts, credentials, internal events and hidden reasoning are never returned. Current Workspace membership is rechecked on every request.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - name: message_id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/Identifier"
        - name: after
          in: query
          schema:
            type: integer
            format: int64
            minimum: 0
            default: 0
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 100
      responses:
        "200":
          description: Safe private Discuss activity.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DiscussRunEventPage"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/workspaces/{workspace_id}/discuss/messages/{message_id}/cancel:
    post:
      tags:
        - Discuss
      operationId: cancelDiscussGeneration
      summary: Request an exact stop for a queued or running response
      description: Queued work becomes cancelled immediately. Running work first reports cancel_requested; its terminal message and run state are reconciled after execution has stopped. A completed action and its receipt are never described as rolled back.
      security:
        - browserSession: []
        - bearerToken: []
        - developerAccessToken: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - $ref: "#/components/parameters/CSRFToken"
        - name: message_id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/Identifier"
      responses:
        "200":
          description: Private canonical Discuss state.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DiscussMessage"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/workspaces/{workspace_id}/discuss/specialist-runs:
    get:
      tags:
        - Discuss
      operationId: listDiscussSpecialistRuns
      summary: List private specialist work linked to Discuss
      description: Returns bounded projections from canonical Agent and hosted work-session journals. Provider transcripts, prompts, leases and internal execution state are excluded.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 50
            default: 20
        - $ref: "#/components/parameters/WorkspaceRealtimeBaseline"
      responses:
        "200":
          description: Private specialist projections.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
            X-Assign-Realtime-Cursor:
              $ref: "#/components/headers/RealtimeReadCursor"
            X-Assign-Realtime-Position:
              $ref: "#/components/headers/RealtimeReadPosition"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DiscussSpecialistRunPage"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/workspaces/{workspace_id}/discuss/specialist-runs/{specialist_run_id}:
    get:
      tags:
        - Discuss
      operationId: getDiscussSpecialistRun
      summary: Get one private Discuss specialist run
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - name: specialist_run_id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/Identifier"
        - $ref: "#/components/parameters/WorkspaceRealtimeBaseline"
      responses:
        "200":
          description: Current canonical specialist projection.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
            X-Assign-Realtime-Cursor:
              $ref: "#/components/headers/RealtimeReadCursor"
            X-Assign-Realtime-Position:
              $ref: "#/components/headers/RealtimeReadPosition"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DiscussSpecialistRun"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/workspaces/{workspace_id}/discuss/specialist-runs/{specialist_run_id}/cancel:
    post:
      tags:
        - Discuss
      operationId: cancelDiscussSpecialistRun
      summary: Request cooperative cancellation of specialist work
      description: Routes cancellation through the bound canonical Agent or work-session journal. A terminal completion may still win a concurrent stop request.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - $ref: "#/components/parameters/CSRFToken"
        - name: specialist_run_id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/Identifier"
      responses:
        "200":
          description: Current canonical specialist projection.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DiscussSpecialistRun"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/workspaces/{workspace_id}/discuss/specialist-runs/{specialist_run_id}/resume:
    post:
      tags:
        - Discuss
      operationId: resumeDiscussSpecialistRun
      summary: Resolve one AG-UI specialist interrupt
      description: Resolves the current canonical work-session clarification or approval. Core reloads the interaction kind and request digest; client metadata cannot widen or replace them. Exact resolved-answer replays are idempotent at the work-session journal.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - $ref: "#/components/parameters/CSRFToken"
        - name: specialist_run_id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/Identifier"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/DiscussSpecialistResumeRequest"
      responses:
        "200":
          description: Current canonical specialist projection after the accepted resolution.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DiscussSpecialistRun"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/workspaces/{workspace_id}/discuss/interactions/{interaction_id}/decision:
    post:
      tags:
        - Discuss
      operationId: decideDiscussInteraction
      summary: Resolve one version-fenced private clarification or approval
      description: The first valid decision wins. Expired interactions or interactions whose bound message revision changed become stale and cannot resume work.
      security:
        - browserSession: []
        - bearerToken: []
        - developerAccessToken: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/IdempotencyKey"
        - name: interaction_id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/Identifier"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/DiscussInteractionDecisionRequest"
      responses:
        "202":
          description: Persisted decision and queued continuation.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DiscussInteractionTurn"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          description: Discuss is not entitled or Workspace AI credits are exhausted.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/workspaces/{workspace_id}/discuss/messages/search:
    get:
      tags:
        - Discuss
      operationId: searchDiscussMessages
      summary: Search the current membership's private Discuss history
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - name: q
          in: query
          required: true
          schema:
            type: string
            minLength: 2
            maxLength: 200
      responses:
        "200":
          description: Bounded private history matches.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DiscussMessagePage"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
  /api/v1/workspaces/{workspace_id}/discuss/read-state:
    post:
      tags:
        - Discuss
      operationId: updateDiscussReadState
      summary: Advance the current membership's Discuss read position
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - $ref: "#/components/parameters/CSRFToken"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/DiscussReadRequest"
      responses:
        "200":
          description: Updated monotonic read state.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DiscussReadState"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
  /api/v1/workspaces/{workspace_id}/discuss/preferences:
    get:
      tags:
        - Discuss
      operationId: getDiscussPreferences
      summary: Read private Discuss schedule, ceremony, delivery, and wording preferences
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
      responses:
        "200":
          description: Current membership-scoped preferences.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DiscussPreferences"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
    patch:
      tags:
        - Discuss
      operationId: updateDiscussPreferences
      summary: Replace private Discuss preferences at an exact revision
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - $ref: "#/components/parameters/CSRFToken"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/DiscussPreferencesUpdate"
      responses:
        "200":
          description: Updated preferences.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DiscussPreferences"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          description: Discuss is not entitled or Workspace AI credits are exhausted.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
  /api/v1/workspaces/{workspace_id}/discuss/reminders:
    get:
      tags:
        - Discuss
      operationId: listDiscussReminders
      summary: List the current membership's private work reminders
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
      responses:
        "200":
          description: Up to 100 private reminders.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DiscussReminderList"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
    post:
      tags:
        - Discuss
      operationId: createDiscussReminder
      summary: Create an explicitly confirmed private work reminder
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - $ref: "#/components/parameters/CSRFToken"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/DiscussReminderCreate"
      responses:
        "201":
          description: Scheduled reminder.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DiscussReminder"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          description: Discuss is not entitled or Workspace AI credits are exhausted.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          $ref: "#/components/responses/NotFound"
  /api/v1/workspaces/{workspace_id}/discuss/reminders/{reminder_id}:
    patch:
      tags:
        - Discuss
      operationId: updateDiscussReminderState
      summary: Complete, cancel, or snooze one private reminder
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - name: reminder_id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/Identifier"
        - $ref: "#/components/parameters/CSRFToken"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/DiscussReminderStateUpdate"
      responses:
        "200":
          description: Updated reminder occurrence.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DiscussReminder"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          description: Discuss is not entitled or Workspace AI credits are exhausted.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
  /api/v1/workspaces/{workspace_id}/tasks/{task_id}/related-context:
    get:
      tags:
        - Tasks
      operationId: getTaskRelatedContext
      summary: Read bounded Workspace Knowledge context related to a Task
      description: Returns a permission-filtered, read-only projection. Globally unconfigured, disabled, unentitled, unacknowledged, empty-scope, and out-of-scope Knowledge conditions use the empty state; unavailable is reserved for genuine retrieval/currentness failures after eligibility. Neither state blocks the owning Task workflow. Indexed subjects may discover Documents through shared distinctive title terms without a cross-domain link generation. Inferred suggestions explain their shared terms and never create or change canonical Task relations.
      security:
        - browserSession: []
      parameters:
        - name: include_code
          in: query
          description: Opt in to read-only indexed code references from repositories bound to this Task Project. Uses deterministic Cognee identifier matching, not semantic model ranking. Code references have no feedback entity ID; open their revision-pinned GitHub URL to review them. Defaults to false for compatibility.
          schema:
            type: boolean
            default: false
        - name: include_commit_mentions
          in: query
          description: Opt in to verified GitHub commit findings from Task comments. Defaults to false so older clients continue receiving only Task and Document findings. Commit lookup runs in the background and does not require Workspace Knowledge indexing to be enabled.
          schema:
            type: boolean
            default: false
        - $ref: "#/components/parameters/WorkspaceId"
        - $ref: "#/components/parameters/WorkspaceRealtimeDerivedBaseline"
        - name: task_id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/Identifier"
      responses:
        "200":
          description: Bounded related-context projection.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
            X-Assign-Realtime-Cursor:
              $ref: "#/components/headers/RealtimeReadCursor"
            X-Assign-Realtime-Position:
              $ref: "#/components/headers/RealtimeReadPosition"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TaskRelatedContext"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/workspaces/{workspace_id}/tasks/{task_id}/suggestion-feedback:
    post:
      tags:
        - Tasks
      operationId: submitTaskSuggestionFeedback
      summary: Confirm or discard one Suggestions-tab finding
      description: Confirming a Task finding creates a canonical `relates` relation; confirming a Document links it. Confirming a commit finding adds the verified commit to Linked development after rechecking the current Task, comment and repository binding. Commit outcomes persist in Core; Task and Document outcomes also relay to Workspace Knowledge. Confirming is idempotent and discarding suppresses the finding for this Task.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - name: task_id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/Identifier"
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SuggestionFeedbackRequest"
      responses:
        "200":
          description: Feedback accepted.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SuggestionFeedbackResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/workspaces/{workspace_id}/billing-settings:
    get:
      tags:
        - Billing
      operationId: getWorkspaceBillingSettings
      summary: Read the provider-neutral Workspace subscription and invoice references
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
      responses:
        "200":
          description: Billing settings and provider availability.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BillingSettingsResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
  /api/v1/me/billing-plans:
    get:
      tags:
        - Billing
      operationId: getAccountBillingPlans
      summary: Read a bounded cross-Workspace billing and plan overview
      description: Returns a signed-cursor page of plan/status facts for at most 100 active Workspace memberships. Sensitive details and mutations remain on the target Workspace Billing surface.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/PageLimit"
      responses:
        "200":
          description: Bounded Account Billing & plans overview.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AccountBillingPlans"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
  /api/v1/workspaces/{workspace_id}/checkout-sessions:
    post:
      tags:
        - Billing
      operationId: createWorkspaceCheckoutSession
      summary: Create a hosted checkout session for a paid Workspace plan
      description: Creates a short-lived provider-hosted Checkout URL. The browser return is navigation only; signed provider webhooks remain authoritative for subscription state and entitlements.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - $ref: "#/components/parameters/CSRFToken"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/BillingCheckoutRequest"
      responses:
        "201":
          description: Hosted Checkout session created.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BillingCheckoutSession"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/workspaces/{workspace_id}/billing-portal-sessions:
    post:
      tags:
        - Billing
      operationId: createWorkspaceBillingPortalSession
      summary: Create a hosted customer billing portal session
      description: Creates a short-lived provider-hosted portal URL for a Workspace that already has a provider customer reference.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - $ref: "#/components/parameters/CSRFToken"
      responses:
        "201":
          description: Hosted billing portal session created.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BillingPortalSession"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/workspaces/{workspace_id}/billing/knowledge-credit-top-ups:
    post:
      tags:
        - Billing
      operationId: createSandboxKnowledgeCreditTopUp
      summary: Create a catalogued Knowledge credit checkout
      description: Requires Workspace billing-management capability, recent authentication, and an Idempotency-Key. A verified paid provider event grants the credits; delayed-payment failure and the browser return never grant value. Production remains unavailable while provider billing is disabled.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SandboxKnowledgeCreditTopUpRequest"
      responses:
        "201":
          description: Provider-hosted credit checkout created.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SandboxKnowledgeCreditTopUp"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/workspaces/{workspace_id}/icon:
    post:
      tags:
        - Workspaces
      operationId: setWorkspaceIconUpload
      summary: Associate a cropped S3-hosted Workspace icon
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/IfMatch"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required:
                - attachment_id
              properties:
                attachment_id:
                  $ref: "#/components/schemas/Identifier"
      responses:
        "200":
          description: Icon associated.
          headers:
            ETag:
              $ref: "#/components/headers/ETag"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WorkspaceIconMutation"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
    delete:
      tags:
        - Workspaces
      operationId: clearWorkspaceIconUpload
      summary: Remove the Workspace icon and retire its stored object
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/IfMatch"
      responses:
        "200":
          description: Icon removed.
          headers:
            ETag:
              $ref: "#/components/headers/ETag"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WorkspaceIconMutation"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
  /api/v1/workspaces/{workspace_id}/icon/content:
    get:
      tags:
        - Workspaces
      operationId: getWorkspaceIconContent
      summary: Redirect to a short-lived inline S3 URL for the Workspace icon
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
      responses:
        "200":
          description: Image bytes after a client follows the redirect.
        "302":
          description: Short-lived object-storage redirect.
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
  /api/v1/workspaces/{workspace_id}/notification-preferences:
    get:
      tags:
        - Account
      operationId: listNotificationPreferences
      summary: List effective notification preferences
      description: Returns every typed category, including inherited defaults, mandatory delivery rules, and whether the source Workspace currently has the paid email entitlement. A revision of `0` means the caller has not overridden that category.
      security:
        - browserSession: []
        - nativeAccessToken: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - name: X-Assign-Realtime-Baseline
          in: header
          required: false
          description: Upcoming browser-session canonical read custody. The named private scope captures this response and a signed replay cursor in one snapshot. Successful responses include X-Assign-Realtime-Cursor and X-Assign-Realtime-Position. Apply all required retained state before acknowledging the corresponding socket checkpoint. Failed or incomplete baselines return no cursor.
          schema:
            type: string
            enum:
              - inbox
      responses:
        "200":
          description: Effective category preferences
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/NotificationPreferenceList"
          headers:
            X-Assign-Realtime-Cursor:
              description: Signed scope/User/session-bound replay custody; only when the requested canonical baseline succeeds.
              schema:
                type: string
                maxLength: 4096
            X-Assign-Realtime-Position:
              description: Decimal-string snapshot position; only with a successful baseline cursor.
              schema:
                type: string
                pattern: ^(0|[1-9][0-9]*)$
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/workspaces/{workspace_id}/project-display-preferences:
    get:
      tags:
        - Account
      operationId: getProjectDisplayPreferences
      summary: Read private Project display preferences for one Workspace
      description: Returns the current Account user's Workspace-scoped Sort and custom sequence. An absent aggregate returns default, an empty sequence and revision 1. Inaccessible, archived and deleted Projects are pruned before returning the canonical revision. Browser-session consumer only; does not change shared Project ranks.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - name: X-Assign-Realtime-Baseline
          in: header
          required: false
          description: Upcoming browser-session canonical read custody. The named private scope captures this response and a signed replay cursor in one snapshot. Successful responses include X-Assign-Realtime-Cursor and X-Assign-Realtime-Position. Apply all required retained state before acknowledging the corresponding socket checkpoint. Failed or incomplete baselines return no cursor.
          schema:
            type: string
            enum:
              - personal_projects
      responses:
        "200":
          description: Canonical private Project display preferences.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
            ETag:
              $ref: "#/components/headers/ETag"
            X-Assign-Realtime-Cursor:
              description: Signed scope/User/session-bound replay custody; only when the requested canonical baseline succeeds.
              schema:
                type: string
                maxLength: 4096
            X-Assign-Realtime-Position:
              description: Decimal-string snapshot position; only with a successful baseline cursor.
              schema:
                type: string
                pattern: ^(0|[1-9][0-9]*)$
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProjectDisplayPreferences"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
    put:
      tags:
        - Account
      operationId: updateProjectDisplayPreferences
      summary: Save private Project display preferences for one Workspace
      description: Atomically saves a private sort and up to 1000 unique readable same-Workspace Project UUIDs. Maximum JSON body is 64 KiB. Active membership and Project-read authority are required. If-Match prevents cross-device overwrites; retry the identical request with the same Idempotency-Key. Shared ranks and global profile revision are unchanged. Unavailable Projects return content-free validation errors.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/IfMatch"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ProjectDisplayPreferencesUpdateRequest"
      responses:
        "200":
          description: Canonical saved Project display preferences.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
            ETag:
              $ref: "#/components/headers/ETag"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProjectDisplayPreferences"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "413":
          $ref: "#/components/responses/PayloadTooLarge"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"
        "428":
          description: If-Match is required to save a preference.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/workspaces/{workspace_id}/project-shortcuts:
    get:
      tags:
        - Account
      operationId: getProjectShortcuts
      summary: Read the current user's Project shortcuts for one Workspace
      description: Returns exactly nine stable slots. Projects that are deleted, archived, or no longer readable are removed before the response is committed. The set is private to the current user and Workspace.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - name: X-Assign-Realtime-Baseline
          in: header
          required: false
          description: Upcoming browser-session canonical read custody. The named private scope captures this response and a signed replay cursor in one snapshot. Successful responses include X-Assign-Realtime-Cursor and X-Assign-Realtime-Position. Apply all required retained state before acknowledging the corresponding socket checkpoint. Failed or incomplete baselines return no cursor.
          schema:
            type: string
            enum:
              - personal_projects
      responses:
        "200":
          description: Current Project shortcut set.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
            ETag:
              $ref: "#/components/headers/ETag"
            X-Assign-Realtime-Cursor:
              description: Signed scope/User/session-bound replay custody; only when the requested canonical baseline succeeds.
              schema:
                type: string
                maxLength: 4096
            X-Assign-Realtime-Position:
              description: Decimal-string snapshot position; only with a successful baseline cursor.
              schema:
                type: string
                pattern: ^(0|[1-9][0-9]*)$
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProjectShortcutSet"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
    put:
      tags:
        - Account
      operationId: updateProjectShortcuts
      summary: Replace the current user's Project shortcuts for one Workspace
      description: Replaces all nine slots atomically. Every assigned Project must still be readable by the caller, Projects cannot be duplicated, and `If-Match` prevents one device from overwriting a newer set from another device.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/IfMatch"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ProjectShortcutUpdateRequest"
      responses:
        "200":
          description: Updated Project shortcut set.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
            ETag:
              $ref: "#/components/headers/ETag"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProjectShortcutSet"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "413":
          $ref: "#/components/responses/PayloadTooLarge"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/me/push-endpoints/browser/config:
    get:
      tags:
        - Account
      operationId: getBrowserPushConfiguration
      summary: Get browser push registration configuration
      description: Returns only the public VAPID application key. The route is unavailable when browser push is not configured.
      security:
        - browserSession: []
      responses:
        "200":
          description: Browser push public configuration
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BrowserPushConfiguration"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
  /api/v1/me/push-endpoints/browser:
    put:
      tags:
        - Account
      operationId: registerBrowserPushEndpoint
      summary: Register or reconfirm this browser session's push subscription
      description: Replaces the current browser session's prior target. Subscription material is encrypted at rest and never returned.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/BrowserCSRFToken"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/BrowserPushEndpointRequest"
      responses:
        "200":
          description: Registered push endpoint metadata
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PushEndpoint"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "409":
          $ref: "#/components/responses/Conflict"
    delete:
      tags:
        - Account
      operationId: deleteBrowserPushEndpoint
      summary: Delete this browser session's push subscription
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/BrowserCSRFToken"
      responses:
        "204":
          description: Push endpoint removed or already absent
        "401":
          $ref: "#/components/responses/Unauthorized"
  /api/v1/workspaces/{workspace_id}/notification-preferences/{category}:
    patch:
      tags:
        - Account
      operationId: updateNotificationPreference
      summary: Update one typed notification preference
      description: |
        Uses `expected_revision: 0` to create the first override and the
        returned positive revision for later compare-and-swap updates.
        Mandatory categories cannot disable Inbox delivery. Their email channel
        is also mandatory when `email_available` is true. A Workspace without
        the paid email entitlement cannot enable email.
      security:
        - browserSession: []
        - nativeAccessToken: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - name: category
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/NotificationCategory"
        - $ref: "#/components/parameters/BrowserCSRFToken"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/NotificationPreferenceUpdateRequest"
      responses:
        "200":
          description: Updated preference
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/NotificationPreference"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/workspaces/{workspace_id}/inbox/read-state:
    post:
      tags:
        - Account
      operationId: convergeInboxReadState
      summary: Converge Inbox seen or read state through a cutoff
      description: Applies an idempotent, monotonic server cutoff so reordered updates from multiple devices cannot make newer items read or regress older state.
      security:
        - browserSession: []
        - nativeAccessToken: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - $ref: "#/components/parameters/BrowserCSRFToken"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/InboxReadStateConvergenceRequest"
      responses:
        "200":
          description: Converged read state and authoritative unread count
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/InboxReadStateConvergence"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/workspace/time-tracking:
    get:
      tags:
        - Time tracking
      operationId: getTimeTrackingPolicy
      summary: Read the Workspace time-tracking policy
      security:
        - browserSession: []
      responses:
        "200":
          description: The current policy, or the disabled default.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TimeTrackingPolicy"
          headers:
            X-Assign-Realtime-Cursor:
              $ref: "#/components/headers/RealtimeReadCursor"
            X-Assign-Realtime-Position:
              $ref: "#/components/headers/RealtimeReadPosition"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
      parameters:
        - $ref: "#/components/parameters/WorkspaceRealtimeBaseline"
    patch:
      tags:
        - Time tracking
      operationId: updateTimeTrackingPolicy
      summary: Update the Workspace time-tracking policy
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/IdempotencyKey"
        - $ref: "#/components/parameters/PolicyIfMatch"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TimeTrackingPolicyUpdateRequest"
      responses:
        "200":
          description: Updated policy
          headers:
            ETag:
              $ref: "#/components/headers/ETag"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TimeTrackingPolicy"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "409":
          $ref: "#/components/responses/Conflict"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/tasks/{task_id}/time-entries:
    get:
      tags:
        - Time tracking
      operationId: listTaskTimeEntries
      summary: List live time entries for a Task in a bounded revision-fenced collection
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/TaskId"
        - $ref: "#/components/parameters/PageLimit"
        - name: cursor
          in: query
          description: Signed Task and revision-bound continuation
          schema:
            type: string
        - $ref: "#/components/parameters/WorkspaceRealtimeBaseline"
      responses:
        "200":
          description: Time entries
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TimeEntryList"
          headers:
            X-Assign-Realtime-Cursor:
              $ref: "#/components/headers/RealtimeReadCursor"
            X-Assign-Realtime-Position:
              $ref: "#/components/headers/RealtimeReadPosition"
            X-Assign-Report-Revision:
              $ref: "#/components/headers/ProjectTimeReportRevision"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "413":
          description: Task time entry collection exceeds the 800-entry retained limit
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
    post:
      tags:
        - Time tracking
      operationId: createTaskTimeEntry
      summary: Log manual time against a Task
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/TaskId"
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TimeEntryCreateRequest"
      responses:
        "201":
          description: Created time entry
          headers:
            ETag:
              $ref: "#/components/headers/ETag"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TimeEntry"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/tasks/{task_id}/time-totals:
    get:
      tags:
        - Time tracking
      operationId: getTaskTimeTotals
      summary: Get Task time totals
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/TaskId"
        - $ref: "#/components/parameters/WorkspaceRealtimeBaseline"
      responses:
        "200":
          description: Exact and reportable Task totals
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TimeTotals"
          headers:
            X-Assign-Report-Revision:
              description: Committed Workspace time report revision for comparing Task entries and totals.
              schema:
                type: string
            X-Assign-Realtime-Cursor:
              $ref: "#/components/headers/RealtimeReadCursor"
            X-Assign-Realtime-Position:
              $ref: "#/components/headers/RealtimeReadPosition"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/projects/{project_id}/time-totals:
    get:
      tags:
        - Time tracking
      operationId: getProjectTimeTotals
      summary: Get Project time totals for a date range
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/ProjectId"
        - name: from
          in: query
          required: true
          schema:
            type: string
            format: date
        - name: to
          in: query
          required: true
          schema:
            type: string
            format: date
        - $ref: "#/components/parameters/WorkspaceRealtimeBaseline"
      responses:
        "200":
          description: Exact and reportable Project totals
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TimeTotals"
          headers:
            X-Assign-Report-Revision:
              $ref: "#/components/headers/ProjectTimeReportRevision"
            X-Assign-Realtime-Cursor:
              $ref: "#/components/headers/RealtimeReadCursor"
            X-Assign-Realtime-Position:
              $ref: "#/components/headers/RealtimeReadPosition"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/projects/{project_id}/time-task-breakdown:
    get:
      tags:
        - Time tracking
      operationId: listProjectTimeTaskBreakdown
      summary: List a Project's recorded time grouped by Task
      description: Groups entries by captured Project attribution. Missing Task metadata does not remove historical totals. Pages are ordered by Task ID and must be recaptured from the beginning after a realtime invalidation. The signed continuation is bound to the Workspace, Actor, Project and date range.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/ProjectId"
        - name: from
          in: query
          required: true
          schema:
            type: string
            format: date
        - name: to
          in: query
          required: true
          schema:
            type: string
            format: date
        - name: cursor
          in: query
          schema:
            type: string
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 100
        - $ref: "#/components/parameters/WorkspaceRealtimeBaseline"
      responses:
        "200":
          description: Authorized bounded Task groups
          headers:
            X-Assign-Report-Revision:
              $ref: "#/components/headers/ProjectTimeReportRevision"
            X-Assign-Realtime-Cursor:
              $ref: "#/components/headers/RealtimeReadCursor"
            X-Assign-Realtime-Position:
              $ref: "#/components/headers/RealtimeReadPosition"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProjectTimeTaskBreakdown"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/projects/{project_id}/time-task-entries:
    get:
      tags:
        - Time tracking
      operationId: listProjectTimeTaskEntries
      summary: List recorded entries for one Task in a Project report
      description: Project managers only. Uses the Project captured when each entry was recorded, even if the Task has moved. Pages are ordered by entry ID and must be recaptured from the beginning after a realtime invalidation. The signed continuation is bound to Workspace, Actor, Project, Task and selected range.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/ProjectId"
        - name: task_id
          in: query
          required: true
          schema:
            type: string
            format: uuid
        - name: from
          in: query
          required: true
          schema:
            type: string
            format: date
        - name: to
          in: query
          required: true
          schema:
            type: string
            format: date
        - name: cursor
          in: query
          schema:
            type: string
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 100
        - $ref: "#/components/parameters/WorkspaceRealtimeBaseline"
      responses:
        "200":
          description: Bounded recorded entries for the selected Task and Project range
          headers:
            X-Assign-Report-Revision:
              $ref: "#/components/headers/ProjectTimeReportRevision"
            X-Assign-Realtime-Cursor:
              $ref: "#/components/headers/RealtimeReadCursor"
            X-Assign-Realtime-Position:
              $ref: "#/components/headers/RealtimeReadPosition"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProjectTimeTaskEntries"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/projects/{project_id}/time-contributor-breakdown:
    get:
      tags:
        - Time tracking
      operationId: listProjectTimeContributorBreakdown
      summary: List a Project's recorded time grouped by contributor
      description: Returns identity-level rows to Project managers and an empty redacted result to other authorized Project readers. Historical Actor attribution is retained when a user is removed; unavailable display names are null. Pages are ordered by Actor ID and must be recaptured from the beginning after a realtime invalidation. The signed continuation is bound to the Workspace, Actor, Project and date range.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/ProjectId"
        - name: from
          in: query
          required: true
          schema:
            type: string
            format: date
        - name: to
          in: query
          required: true
          schema:
            type: string
            format: date
        - name: cursor
          in: query
          schema:
            type: string
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 100
        - $ref: "#/components/parameters/WorkspaceRealtimeBaseline"
      responses:
        "200":
          description: Bounded contributor groups or a redacted empty result
          headers:
            X-Assign-Report-Revision:
              $ref: "#/components/headers/ProjectTimeReportRevision"
            X-Assign-Realtime-Cursor:
              $ref: "#/components/headers/RealtimeReadCursor"
            X-Assign-Realtime-Position:
              $ref: "#/components/headers/RealtimeReadPosition"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProjectTimeContributorBreakdown"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/time-entries/{entry_id}:
    patch:
      tags:
        - Time tracking
      operationId: updateTimeEntry
      summary: Correct a time entry
      security:
        - browserSession: []
      parameters:
        - name: entry_id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/Identifier"
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/IdempotencyKey"
        - $ref: "#/components/parameters/IfMatch"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TimeEntryUpdateRequest"
      responses:
        "200":
          description: Updated entry
          headers:
            ETag:
              $ref: "#/components/headers/ETag"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TimeEntry"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "409":
          $ref: "#/components/responses/Conflict"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
    delete:
      tags:
        - Time tracking
      operationId: voidTimeEntry
      summary: Void a time entry
      security:
        - browserSession: []
      parameters:
        - name: entry_id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/Identifier"
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/IdempotencyKey"
        - $ref: "#/components/parameters/IfMatch"
      responses:
        "200":
          description: Voided entry
          headers:
            ETag:
              $ref: "#/components/headers/ETag"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TimeEntry"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "409":
          $ref: "#/components/responses/Conflict"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/timesheets/me:
    get:
      tags:
        - Time tracking
      operationId: getMyTimesheet
      summary: List the caller's time entries in a date range
      description: Returns up to 100 entries per page with stable totals for the selected range. Follow next with the same range and limit. A changed report revision refuses continuation; ranges over 800 entries return 413 at the retained limit.
      security:
        - browserSession: []
      parameters:
        - name: from
          in: query
          required: true
          schema:
            type: string
            format: date
        - name: to
          in: query
          required: true
          schema:
            type: string
            format: date
        - $ref: "#/components/parameters/PageLimit"
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/WorkspaceRealtimeBaseline"
      responses:
        "200":
          description: Personal timesheet
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Timesheet"
          headers:
            X-Assign-Realtime-Cursor:
              $ref: "#/components/headers/RealtimeReadCursor"
            X-Assign-Realtime-Position:
              $ref: "#/components/headers/RealtimeReadPosition"
            X-Assign-Report-Revision:
              description: Snapshot revision shared by every page of the selected personal timesheet.
              schema:
                type: string
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "409":
          $ref: "#/components/responses/Conflict"
        "413":
          $ref: "#/components/responses/PayloadTooLarge"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/timesheets/me/export:
    get:
      tags:
        - Time tracking
      operationId: exportMyTimesheet
      summary: Export the caller's time entries as CSV
      description: Exports one consistent selected range of at most 800 entries; a larger range returns 413.
      security:
        - browserSession: []
      parameters:
        - name: from
          in: query
          required: true
          schema:
            type: string
            format: date
        - name: to
          in: query
          required: true
          schema:
            type: string
            format: date
        - $ref: "#/components/parameters/PageLimit"
        - $ref: "#/components/parameters/WorkspaceRealtimeBaseline"
      responses:
        "200":
          description: CSV timesheet export
          content:
            text/csv:
              schema:
                type: string
                format: binary
          headers:
            X-Assign-Realtime-Cursor:
              $ref: "#/components/headers/RealtimeReadCursor"
            X-Assign-Realtime-Position:
              $ref: "#/components/headers/RealtimeReadPosition"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "413":
          $ref: "#/components/responses/PayloadTooLarge"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/auth/register:
    post:
      tags:
        - Authentication
      operationId: register
      summary: Create an account
      description: |
        Creates a user and password credential, then issues an account-only
        browser session. No Workspace is created during registration. The
        caller must create their first Workspace through
        `POST /api/v1/workspaces/bootstrap` before using Workspace-scoped
        operations, or accept a Workspace invitation. Without `invitation_token`,
        the address starts unverified and a verification message is sent.
        A valid pending invitation for the supplied email verifies that address
        without a second email. Registration does not consume the invitation
        or grant membership; accepting it remains a separate explicit action.
        Invalid, expired, revoked, used or mismatched invitation credentials
        return `400 invalid_invitation` without creating an account.

        Verification gates account **recovery**, not access: a password reset
        is never issued for an unverified address, because whoever controls
        that inbox has not proven they own the account.

        A taken address is reported plainly. Registration necessarily reveals
        whether an address can be registered, so a vague failure would only
        strand a user who already has an account.
      security: []
      responses:
        "201":
          description: |
            The account was created and signed in. Two `Set-Cookie` fields
            establish `__Host-assign_session` and `__Host-assign_csrf`.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
            X-Request-ID:
              $ref: "#/components/headers/RequestId"
            X-Content-Type-Options:
              $ref: "#/components/headers/ContentTypeOptions"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AuthenticatedAccount"
        "400":
          $ref: "#/components/responses/PasswordRejected"
        "409":
          $ref: "#/components/responses/Conflict"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/RegisterRequest"
  /api/v1/auth/login:
    post:
      tags:
        - Authentication
      operationId: login
      summary: Sign in with an email address and password
      description: |
        Verifies the password. When the account has no second factor this
        issues a browser session directly. When it does, the response instead
        carries a short-lived challenge and **no session is created**: the
        caller must complete `verifyMFA` before anything is readable.

        Both outcomes are `200`, and the two bodies are told apart by the
        `mfa_required` field. A client must branch on it rather than assuming
        a session exists.

        Every failure returns the same `401 invalid_credentials` whatever went
        wrong — unknown address, no password credential on the account, or a
        wrong password — and the server spends comparable time in each case.
        Distinguishing them would turn this endpoint into an oracle for which
        addresses are registered.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/LoginRequest"
      responses:
        "200":
          description: |
            Either the session was created — two `Set-Cookie` fields establish
            `__Host-assign_session` and `__Host-assign_csrf` — or a second
            factor is required, in which case no cookies are set and the body
            carries the challenge.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
            X-Request-ID:
              $ref: "#/components/headers/RequestId"
            X-Content-Type-Options:
              $ref: "#/components/headers/ContentTypeOptions"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LoginResult"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/auth/email/verify:
    post:
      tags:
        - Authentication
      operationId: verifyEmail
      summary: Verify an email address
      description: |
        Redeems the single-use token carried by the verification email link.
        Redemption is atomic, so a
        replayed link fails rather than verifying twice, and an expired or
        unknown token is refused identically to a spent one.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TokenRequest"
      responses:
        "204":
          description: The address is verified.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
        "400":
          $ref: "#/components/responses/BadRequest"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/auth/email/resend-verification:
    post:
      tags:
        - Authentication
      operationId: resendVerification
      summary: Send a new verification message
      description: |
        Issues a fresh verification token and retires any outstanding one, so
        a link forwarded from an older message stops working.

        The response is `202` and empty whether or not the address is
        registered, whether or not it is already verified, and whether or not
        delivery succeeded. Anything else would confirm an address exists.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/EmailRequest"
      responses:
        "202":
          description: |
            The request was accepted. This says nothing about whether the
            address is registered or whether a message was sent.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
        "400":
          $ref: "#/components/responses/BadRequest"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/auth/password/forgot:
    post:
      tags:
        - Authentication
      operationId: forgotPassword
      summary: Request a password reset
      description: |
        Issues a single-use reset token to a **verified** address, retires any
        outstanding one, and emails a link to `/reset-password` carrying the
        token. The link is valid for four hours. No reset token is issued for
        an unverified address: whoever controls that inbox has not proven they
        own the account. Instead, Assign sends a fresh verification email link
        so the account holder can verify the address and then request a reset.

        Like resend-verification, the response is `202` and empty in every
        case, including delivery failure.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/EmailRequest"
      responses:
        "202":
          description: |
            The request was accepted. This says nothing about whether the
            address is registered, verified, or reachable.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
        "400":
          $ref: "#/components/responses/BadRequest"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/auth/password/reset:
    post:
      tags:
        - Authentication
      operationId: resetPassword
      summary: Reset a password with a token
      description: |
        Redeems a single-use reset token and replaces the password.

        A reset revokes **every** session for the account, including the one
        making the request: a reset is what an account-takeover victim
        performs, so the attacker's session must not survive it, and the
        holder of a mailed token has not otherwise authenticated. Both
        browser-session cookies are expired in the response.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PasswordResetRequest"
      responses:
        "204":
          description: |
            The password was replaced and every session revoked. Two
            `Set-Cookie` fields expire `__Host-assign_session` and
            `__Host-assign_csrf`.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
        "400":
          $ref: "#/components/responses/PasswordRejected"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/auth/password/change:
    post:
      tags:
        - Authentication
      operationId: changePassword
      summary: Change the current user's password
      description: |
        Replaces the password for the authenticated user. The current password
        is required: an unattended browser must not be enough to lock the
        owner out.

        Unlike a reset, a change **keeps the session that performed it** and
        revokes every other session, so the user is not signed out of the
        browser they are using.

        An account that signs in only through an identity provider has no
        password to change and receives `409`.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/CSRFToken"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PasswordChangeRequest"
      responses:
        "204":
          description: The password was replaced and other sessions revoked.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
        "400":
          $ref: "#/components/responses/PasswordRejected"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "409":
          $ref: "#/components/responses/Conflict"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/auth/passkeys/login/options:
    post:
      tags:
        - Authentication
      operationId: beginPasskeyLogin
      summary: Start a passkey sign-in
      description: |
        Returns the options to pass to `navigator.credentials.get()`, together
        with a single-use ceremony token to return with the assertion.

        The ceremony is **discoverable**: it names no account and lists no
        allowed credentials, because the authenticator is what proposes an
        account. That is what makes passkey sign-in usernameless, and it is
        also why this unauthenticated endpoint cannot be used to ask whether a
        given credential or account exists — the response is the same for every
        caller.

        Passkey operations are served only by a deployment with a configured
        relying party. Where none is configured they return `404`, like any
        other unrouted path.
      security: []
      responses:
        "201":
          description: |
            A ceremony was started. Pass `options` to the browser unchanged; it
            is the WebAuthn structure the credential API expects.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
            X-Request-ID:
              $ref: "#/components/headers/RequestId"
            X-Content-Type-Options:
              $ref: "#/components/headers/ContentTypeOptions"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PasskeyCeremony"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/auth/passkeys/login/verify:
    post:
      tags:
        - Authentication
      operationId: verifyPasskeyLogin
      summary: Complete a passkey sign-in
      description: |
        Verifies the authenticator's assertion and issues a browser session.

        **No second factor is requested.** A passkey is strong authentication
        on its own — the authenticator performed user verification, which is
        the assurance a second factor exists to add — so unlike a password
        sign-in this never returns a challenge. A successful response is always
        the account.

        Every failure is `401`, whether the ceremony was unknown, expired, or
        already used, or the assertion did not verify. The distinctions are
        useful only to someone probing the endpoint; an honest client can only
        start a new ceremony.

        A credential whose signature counter fails to advance is treated as
        cloned: it is disabled, and this and every later attempt with it are
        refused. The account's other sign-in methods continue to work, and the
        credential remains listed — marked `disabled` — so the user can see
        what happened.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PasskeyAssertionRequest"
      responses:
        "200":
          description: |
            The assertion verified and the session was created. Two
            `Set-Cookie` fields establish `__Host-assign_session` and
            `__Host-assign_csrf`.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
            X-Request-ID:
              $ref: "#/components/headers/RequestId"
            X-Content-Type-Options:
              $ref: "#/components/headers/ContentTypeOptions"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AuthenticatedAccount"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/auth/passkeys/register/options:
    post:
      tags:
        - Authentication
      operationId: beginPasskeyRegistration
      summary: Start registering a passkey
      description: |
        Returns the options to pass to `navigator.credentials.create()`,
        together with a single-use ceremony token.

        The options require a discoverable credential and user verification,
        which is what makes the resulting passkey usable for usernameless
        sign-in. Credentials already registered to the account are excluded, so
        an authenticator that is already enrolled declines rather than creating
        a duplicate.

        The ceremony is bound to the calling account and can only be completed
        by it. An account at the ten-passkey limit receives `409`.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/CSRFToken"
      responses:
        "201":
          description: A ceremony was started.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
            X-Request-ID:
              $ref: "#/components/headers/RequestId"
            X-Content-Type-Options:
              $ref: "#/components/headers/ContentTypeOptions"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PasskeyCeremony"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "409":
          $ref: "#/components/responses/Conflict"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/auth/passkeys/register/verify:
    post:
      tags:
        - Authentication
      operationId: verifyPasskeyRegistration
      summary: Finish registering a passkey
      description: |
        Verifies the authenticator's attestation and stores the credential
        under the supplied name.

        The account is taken from the session, never from the request: a
        body-supplied user would let a caller enrol a credential onto someone
        else's account. A ceremony started by a different account is refused.

        Registering a passkey does not remove any other sign-in method. It also
        does not require the account to have one — but the account's **last**
        remaining method cannot later be deleted.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/CSRFToken"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PasskeyAttestationRequest"
      responses:
        "201":
          description: The passkey is registered and usable for sign-in.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
            X-Request-ID:
              $ref: "#/components/headers/RequestId"
            X-Content-Type-Options:
              $ref: "#/components/headers/ContentTypeOptions"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Passkey"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "409":
          $ref: "#/components/responses/Conflict"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/auth/passkeys:
    get:
      tags:
        - Authentication
      operationId: listPasskeys
      summary: List the current account's passkeys
      description: |
        Returns every registered passkey, including any disabled by a clone
        signal — a disabled credential stays listed because that is how the
        user finds out something happened to it.

        The list is bounded by the ten-passkey registration limit and is not
        paginated. Public keys and attestation data are deliberately absent: a
        client has no use for them.
      security:
        - browserSession: []
      responses:
        "200":
          description: The account's passkeys.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
            X-Request-ID:
              $ref: "#/components/headers/RequestId"
            X-Content-Type-Options:
              $ref: "#/components/headers/ContentTypeOptions"
            X-Assign-Realtime-Cursor:
              description: Signed scope/User/session-bound replay custody; only when the requested canonical baseline succeeds.
              schema:
                type: string
                maxLength: 4096
            X-Assign-Realtime-Position:
              description: Decimal-string snapshot position; only with a successful baseline cursor.
              schema:
                type: string
                pattern: ^(0|[1-9][0-9]*)$
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PasskeyList"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
      parameters:
        - name: X-Assign-Realtime-Baseline
          in: header
          required: false
          description: Upcoming browser-session canonical read custody. The named private scope captures this response and a signed replay cursor in one snapshot. Successful responses include X-Assign-Realtime-Cursor and X-Assign-Realtime-Position. Apply all required retained state before acknowledging the corresponding socket checkpoint. Failed or incomplete baselines return no cursor.
          schema:
            type: string
            enum:
              - account_security
  /api/v1/auth/passkeys/{passkey_id}:
    parameters:
      - name: passkey_id
        in: path
        required: true
        schema:
          $ref: "#/components/schemas/Identifier"
    patch:
      tags:
        - Authentication
      operationId: renamePasskey
      summary: Rename a passkey
      description: |
        Changes the user's own label for a passkey. The name is a display
        string only and has no effect on the credential.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/CSRFToken"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PasskeyRenameRequest"
      responses:
        "200":
          description: The passkey was renamed.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
            X-Request-ID:
              $ref: "#/components/headers/RequestId"
            X-Content-Type-Options:
              $ref: "#/components/headers/ContentTypeOptions"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Passkey"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
    delete:
      tags:
        - Authentication
      operationId: deletePasskey
      summary: Remove a passkey
      description: |
        Removes a passkey from the account. This does not tell the
        authenticator to forget its copy; the user removes that separately in
        their platform's own passkey settings.

        A passkey belonging to another account is `404`, the same answer a
        malformed identifier receives — a passkey identifier belongs to exactly
        one account, so the two must not be distinguishable.

        Removing the account's **last remaining sign-in method** is refused with
        `409 last_sign_in_method`. An account with a passkey and no password
        could otherwise lock itself out in a single request with nothing left to
        recover with. A password, a linked identity provider, or another usable
        passkey each satisfy the requirement; a passkey disabled by a clone
        signal does not, because it is not a way back in.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/CSRFToken"
      responses:
        "204":
          description: The passkey was removed.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/auth/mfa/verify:
    post:
      tags:
        - Authentication
      operationId: verifyMFA
      summary: Complete a sign-in with a second factor
      description: |
        Exchanges the challenge from `login` and a code for a browser session.
        This is the only place a challenge becomes a session; a challenge on
        its own reads nothing.

        The code may be a TOTP code or one of the account's recovery codes.
        Recovery codes are matched ignoring spacing and case, and each is
        accepted exactly once.

        A code is refused identically whether it was a wrong TOTP value, an
        unrecognized recovery code, or one already spent — telling them apart
        would report which factors the account actually has. A TOTP code is
        also refused on its second use, so a code observed in transit is not
        replayable for the rest of its window.

        The challenge counts its own attempts and gives up after five, because
        a six-digit code is reachable by brute force within the challenge
        lifetime if it never does. An exhausted or expired challenge returns
        `401 invalid_challenge`, and the caller must sign in again.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/MFAVerifyRequest"
      responses:
        "200":
          description: |
            The second factor verified and the session was created. Two
            `Set-Cookie` fields establish `__Host-assign_session` and
            `__Host-assign_csrf`.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
            X-Request-ID:
              $ref: "#/components/headers/RequestId"
            X-Content-Type-Options:
              $ref: "#/components/headers/ContentTypeOptions"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AuthenticatedAccount"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/auth/mfa/totp/setup:
    post:
      tags:
        - Authentication
      operationId: setupTOTP
      summary: Begin enrolling a time-based one-time password factor
      description: |
        Generates a shared secret and returns it once, with the `otpauth://`
        URI a QR code encodes. **The secret is never readable again**; a client
        that loses it must start enrolment over.

        Setup alone does not enable the factor — sign-in keeps working
        unchanged until `confirmTOTP` succeeds, so an abandoned enrolment
        cannot lock anyone out. Restarting setup replaces any unconfirmed
        secret, which stops a previously displayed QR code from working.

        An account that already has a confirmed factor receives `409`: allowing
        a replacement would let whoever holds a live session swap the second
        factor out silently.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/CSRFToken"
      responses:
        "201":
          description: A secret was generated and stored unconfirmed.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
            X-Request-ID:
              $ref: "#/components/headers/RequestId"
            X-Content-Type-Options:
              $ref: "#/components/headers/ContentTypeOptions"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TOTPSetup"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "409":
          $ref: "#/components/responses/Conflict"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/auth/mfa/totp/confirm:
    post:
      tags:
        - Authentication
      operationId: confirmTOTP
      summary: Enable the time-based one-time password factor
      description: |
        Enables the factor once a code from the enrolling authenticator
        matches, and returns the account's ten recovery codes. Like the secret,
        **the codes are shown exactly once** — only their hashes are stored.

        The code used here is spent by the confirmation, so it cannot
        immediately be replayed as a sign-in.

        Calling this without a preceding setup returns `409 mfa_not_set_up`;
        calling it when a factor is already confirmed returns
        `409 mfa_already_enabled`.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/CSRFToken"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/MFACodeRequest"
      responses:
        "201":
          description: |
            The factor is enabled and sign-in now requires it. The recovery
            codes in this response are not retrievable afterwards.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
            X-Request-ID:
              $ref: "#/components/headers/RequestId"
            X-Content-Type-Options:
              $ref: "#/components/headers/ContentTypeOptions"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RecoveryCodes"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "409":
          $ref: "#/components/responses/Conflict"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/auth/mfa/totp:
    delete:
      tags:
        - Authentication
      operationId: disableTOTP
      summary: Remove the time-based one-time password factor
      description: |
        Removes the factor and every recovery code that went with it —
        recovery codes exist only to recover a factor, so leaving them would
        keep live bearer secrets for something that no longer exists.

        Removal requires the current password **and** a current code (TOTP or
        recovery). Either alone would let a stolen session or a stolen phone
        take the protection off by itself.

        An account with no confirmed factor receives `409 mfa_not_enabled`; an
        unconfirmed enrolment is not a factor for this purpose.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/CSRFToken"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/MFADisableRequest"
      responses:
        "204":
          description: The factor and its recovery codes were removed.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "409":
          $ref: "#/components/responses/Conflict"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/auth/mfa/recovery-codes/regenerate:
    post:
      tags:
        - Authentication
      operationId: regenerateRecoveryCodes
      summary: Replace the account's recovery codes
      description: |
        Issues a fresh set of ten codes and invalidates every previous one,
        including any still unused. Like the original set, they are shown
        exactly once.

        The current password is required, because the codes this returns are
        themselves a way past the second factor. An account with no confirmed
        factor receives `409 mfa_not_enabled`.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/CSRFToken"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/MFARegenerateRequest"
      responses:
        "201":
          description: |
            A new set of recovery codes was issued and the previous set
            invalidated.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
            X-Request-ID:
              $ref: "#/components/headers/RequestId"
            X-Content-Type-Options:
              $ref: "#/components/headers/ContentTypeOptions"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RecoveryCodes"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "409":
          $ref: "#/components/responses/Conflict"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/auth/csrf:
    get:
      tags:
        - Authentication
      operationId: getBrowserCSRFToken
      summary: Read the current session-bound CSRF token
      description: |
        Opt-in direct browser ingress. Requires an exact configured Origin,
        the API-host session cookie and its valid session-bound CSRF cookie.
        Returns no-store JSON for an application on a separate same-site origin.
        Does not rotate or refresh the session. Browser-managed Origin is never
        replaced with an arbitrary return URL. Existing same-origin clients
        continue reading their own CSRF cookie. Unconfigured deployments omit
        this operation.
      security:
        - browserSession: []
      parameters:
        - name: Origin
          in: header
          required: true
          description: Browser-managed exact application origin admitted by Core.
          schema:
            type: string
            format: uri
      responses:
        "200":
          description: The authenticated session's CSRF token.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
          content:
            application/json:
              schema:
                type: object
                required:
                  - csrf_token
                additionalProperties: false
                properties:
                  csrf_token:
                    type: string
                    minLength: 1
                    maxLength: 1024
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/auth/continuations/{kind}:
    get:
      tags:
        - Authentication
      operationId: captureBrowserContinuation
      summary: Capture an authentication link in API-host HttpOnly custody
      description: |
        Opt-in browser navigation for the configured Astro application. Captures
        a link token in a purpose-bound encrypted HttpOnly host-only cookie for
        ten minutes, then redirects to the fixed application route with an opaque
        continuation marker. Email-change capture additionally requires the
        current browser session and binds redemption to that account. Existing
        raw-token and canonical Web/native flows remain valid. A marker submitted
        to its original POST operation requires the matching cookie and kind;
        body size is bounded to 65,536 bytes. Successful domain redemption clears
        the cookie and the original domain token enforces single use. Invitation
        registration retains custody for the separate acceptance operation.
        Tokens and continuation cookies must never enter logs or public caches.
      security: []
      parameters:
        - name: kind
          in: path
          required: true
          schema:
            type: string
            enum:
              - password
              - email
              - email_change
              - invitation
        - name: token
          in: query
          required: true
          description: Original link credential; at most 2,000 UTF-8 bytes.
          schema:
            type: string
            minLength: 1
            maxLength: 2000
      responses:
        "303":
          description: Token-free marker navigation to the exact configured application origin.
          headers:
            Location:
              description: Fixed kind-specific application route with an opaque continuation marker.
              schema:
                type: string
                format: uri
            Set-Cookie:
              description: Secure HttpOnly SameSite=Lax host-only continuation cookie; Max-Age=600.
              schema:
                type: string
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/auth/logout:
    post:
      tags:
        - Authentication
      operationId: logout
      summary: Log out the current browser session
      description: |
        Revokes the current browser session and expires both browser-session
        cookies. The CSRF header must exactly match the host-only CSRF cookie
        and the token bound to the authenticated session.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/CSRFToken"
      responses:
        "204":
          description: |
            The session was revoked. Two `Set-Cookie` fields expire
            `__Host-assign_session` and `__Host-assign_csrf` immediately.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
            X-Request-ID:
              $ref: "#/components/headers/RequestId"
            X-Content-Type-Options:
              $ref: "#/components/headers/ContentTypeOptions"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/auth/providers/{provider}/authorize:
    x-assign-ingress-aliases:
      - path: /api/v1/auth/preview/providers/{provider}/authorize
        providers:
          - google
        configurationKey: ASSIGN_ASTRO_PREVIEW_ORIGIN
        purpose: Internal static-preview ingress mapping; retains this browser contract with a separately bound callback origin.
      - path: /api/v1/auth/astro/providers/{provider}/authorize
        providers:
          - google
          - github
        configurationKey: ASSIGN_ASTRO_APP_ORIGIN
        purpose: Opt-in direct API-host profile reusing registered Google/GitHub callbacks through a bounded Core handoff. An encrypted return cookie binds state/provider/application before code exchange; canonical and native flows remain unchanged.
    get:
      tags:
        - Authentication
      operationId: beginProviderSignIn
      summary: Start provider sign-in
      description: |
        Begins an OAuth/OIDC sign-in attempt with the named provider and
        redirects the browser to that provider's consent screen. An unknown
        or disabled provider name reports the same not-found response as any
        other absent resource.
      security: []
      parameters:
        - $ref: "#/components/parameters/Provider"
        - name: return_to
          in: query
          required: false
          description: Local browser path to resume after provider sign-in. Accepts `/oauth/authorize` with a query string, token-free `/invitations/accept`, or `/app/` Workspace paths including query and fragment. External URLs and unapproved paths are ignored.
          schema:
            type: string
            maxLength: 4096
            example: /oauth/authorize?client_id=https%3A%2F%2Fchatgpt.com%2Foauth%2Fcodex%2Fclient.json
        - name: remember_me
          in: query
          required: false
          description: 'Pass `false` to decline "Remember this device": the session issued when the provider callback completes is a browser-session cookie bounded to 48 hours (idle and absolute). Any other value, or omitting it, issues the persistent session.'
          schema:
            type: boolean
      responses:
        "302":
          description: Redirects to the provider's authorization endpoint.
          headers:
            Location:
              description: The provider's authorization URL.
              schema:
                type: string
                format: uri
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/auth/providers/{provider}/callback:
    x-assign-ingress-aliases:
      - path: /api/v1/auth/preview/providers/{provider}/callback
        providers:
          - google
        configurationKey: ASSIGN_ASTRO_PREVIEW_ORIGIN
        purpose: Internal static-preview ingress mapping; retains this browser contract and host-only session cookies.
      - path: /api/v1/auth/astro/providers/{provider}/callback
        providers:
          - google
          - github
        configurationKey: ASSIGN_ASTRO_APP_ORIGIN
        purpose: Opt-in API-host GET callback after Core routes a marked ceremony from its existing registered provider callback. Requires an unexpired encrypted cookie matching state, provider and the exact application origin before completion. Canonical, preview and native/link callbacks retain their owners.
    get:
      tags:
        - Authentication
      operationId: completeProviderSignInCallback
      summary: Complete a provider ceremony (redirect callback)
      description: |
        Completes an attempt from a provider that redirects with a query
        string (Google, GitHub).

        The route serves two ceremonies, and which one runs is decided from
        server-side state, never from the request. A sign-in sets both
        browser-session cookies and, by default, redirects to the existing
        /app launch entry. The browser resolves the exact active Workspace ID
        to its readable path before opening Projects. Accounts without a
        Workspace continue to Workspace creation; validated explicit
        continuations retain their destination. It first
        resolves an exact `(provider, provider_subject)` identity. If none
        exists and the provider supplies a verified email matching an existing
        normalized account address, the callback atomically attaches the new
        identity and signs in that existing account; an absent or unverified
        email cannot do so. An identity link started from
        `POST /api/v1/me/identities` issues no session and rewrites no cookie —
        the caller is already signed in — and redirects to the identity
        settings surface instead. A ceremony is claimed by a single
        conditional update with its purpose in the predicate, so a link can
        never be redeemed as a sign-in and a sign-in can never be redeemed as
        an explicit account-settings link ceremony.

        On failure the response still redirects, to the web application's
        login page with an opaque error indicator, so the content-free
        authentication boundary is preserved even in a browser navigation.
      security: []
      parameters:
        - $ref: "#/components/parameters/Provider"
        - name: state
          in: query
          required: true
          schema:
            type: string
        - name: code
          in: query
          required: true
          schema:
            type: string
      responses:
        "302":
          $ref: "#/components/responses/AuthenticationRedirect"
        "400":
          $ref: "#/components/responses/BadRequest"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
    post:
      tags:
        - Authentication
      operationId: completeProviderSignInCallbackForm
      summary: Complete a provider ceremony (form_post callback)
      description: |
        Completes an attempt from a provider that posts form-encoded
        parameters instead of redirecting with a query string (Apple).
        Behavior, including the sign-in and identity-link branch, is
        otherwise identical to the `GET` callback.
      security: []
      parameters:
        - $ref: "#/components/parameters/Provider"
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              additionalProperties: false
              required:
                - state
                - code
              properties:
                state:
                  type: string
                code:
                  type: string
      responses:
        "302":
          $ref: "#/components/responses/AuthenticationRedirect"
        "400":
          $ref: "#/components/responses/BadRequest"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/dev/oauth/authorize:
    get:
      tags:
        - Authentication
      operationId: authorizeDeveloperClient
      summary: Authorize a registered first-party developer client
      description: |
        Completes first-party CLI or IDE authorization from a signed-in
        system browser. The request must use Authorization Code with PKCE
        `S256`, a registered client ID (`assign-cli`, `assign-jetbrains`, or
        `assign-vscode`),
        and an exact `http` loopback callback
        on `127.0.0.1` or `[::1]`, an ephemeral port, and path `/callback`.
        Client secrets, `localhost`, custom URI schemes, non-loopback hosts,
        alternate paths, query-bearing callbacks, and PKCE downgrade are
        rejected. The five-minute code is single-use.
      security:
        - browserSession: []
      parameters:
        - name: response_type
          in: query
          required: true
          schema:
            type: string
            const: code
        - name: client_id
          in: query
          required: true
          schema:
            $ref: "#/components/schemas/DeveloperOAuthClientId"
        - name: redirect_uri
          in: query
          required: true
          schema:
            $ref: "#/components/schemas/DeveloperLoopbackRedirectUri"
        - name: state
          in: query
          required: true
          schema:
            $ref: "#/components/schemas/OAuthState"
        - name: code_challenge
          in: query
          required: true
          schema:
            $ref: "#/components/schemas/PKCEChallenge"
        - name: code_challenge_method
          in: query
          required: true
          schema:
            type: string
            const: S256
      responses:
        "302":
          description: Redirect to the exact bound loopback callback with one code and unchanged state.
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalServerError"
  /api/v1/dev/oauth/token:
    post:
      tags:
        - Authentication
      operationId: exchangeDeveloperOAuthToken
      summary: Exchange a developer-client authorization code or refresh token
      description: |
        Exchanges one bound authorization code and PKCE verifier, or rotates
        the current refresh token. Access tokens expire after 15 minutes.
        Refresh families expire after 90 days of inactivity and one year
        absolutely; replay of a consumed refresh token revokes the family.
        No client secret is accepted.
      security: []
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              $ref: "#/components/schemas/DeveloperOAuthTokenRequest"
      responses:
        "200":
          description: A short-lived API access token and rotated refresh token.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DeveloperOAuthTokenSet"
        "400":
          $ref: "#/components/responses/BadRequest"
        "500":
          $ref: "#/components/responses/InternalServerError"
  /api/v1/dev/oauth/revoke:
    post:
      tags:
        - Authentication
      operationId: revokeCurrentDeveloperCredential
      summary: Revoke the calling interactive developer-client credential
      description: Revokes the complete refresh family. The client removes local credentials even when remote revocation cannot be confirmed.
      security:
        - developerAccessToken: []
      responses:
        "204":
          description: The developer-client credential is revoked
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalServerError"
  /api/v1/mobile/workspaces/bootstrap:
    post:
      tags:
        - Workspaces
      operationId: bootstrapNativeMobileWorkspace
      summary: Create the native caller's first Workspace
      description: |
        Runs the same first-Workspace creation service and serialized account
        membership check as browser bootstrap. Existing memberships cause 409.
        Returns the Workspace without creating, switching or setting browser
        cookies. The native app selects the returned Workspace explicitly.
        No additional paid-plan or general Workspace-creation operation is admitted.
      security:
        - nativeAccessToken: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/BootstrapWorkspaceRequest"
      responses:
        "201":
          description: First Workspace and owner membership created. No browser cookies.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Workspace"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "409":
          $ref: "#/components/responses/Conflict"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/mobile/auth/login:
    post:
      tags:
        - Authentication
      operationId: loginNativeMobile
      summary: Log in from the native password screen
      description: |
        First-party native email/password authentication. No browser, callback,
        cookies, client secret or platform-store registration is involved.
        Use the fixed client matching the platform and the installation binding.
        Core preserves password policy, account state, rate limits and MFA.
        Login returns mfa_required=true with a five-minute, five-attempt challenge
        when enrolled; no credential is issued before verification. Native MFA
        challenges cannot be redeemed by browser MFA or another installation.
        Registration creates an unverified account and sends verification email;
        verification gates recovery, not access. A verification_delivery_failed
        response means the account exists; request another email and log in.
        Passwords and challenges are memory-only; never log or automatically retry.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/NativePasswordLoginRequest"
      responses:
        "200":
          description: Native authentication result. No Set-Cookie headers.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/NativePasswordLoginResult"
        "400":
          $ref: "#/components/responses/PasswordRejected"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "409":
          $ref: "#/components/responses/Conflict"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          description: Maintenance or verification_delivery_failed after account creation.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /api/v1/mobile/auth/register:
    post:
      tags:
        - Authentication
      operationId: registerNativeMobile
      summary: Register from the native account screen
      description: |
        First-party native email/password authentication. No browser, callback,
        cookies, client secret or platform-store registration is involved.
        Use the fixed client matching the platform and the installation binding.
        Core preserves password policy, account state, rate limits and MFA.
        Login returns mfa_required=true with a five-minute, five-attempt challenge
        when enrolled; no credential is issued before verification. Native MFA
        challenges cannot be redeemed by browser MFA or another installation.
        Registration creates an unverified account and sends verification email;
        verification gates recovery, not access. A verification_delivery_failed
        response means the account exists; request another email and log in.
        Passwords and challenges are memory-only; never log or automatically retry.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/NativePasswordRegisterRequest"
      responses:
        "201":
          description: Native authentication result. No Set-Cookie headers.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/NativeMobileTokenSet"
        "400":
          $ref: "#/components/responses/PasswordRejected"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "409":
          $ref: "#/components/responses/Conflict"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          description: Maintenance or verification_delivery_failed after account creation.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /api/v1/mobile/auth/mfa/verify:
    post:
      tags:
        - Authentication
      operationId: verifyNativeMobileMFA
      summary: Complete installation-bound native MFA
      description: |
        First-party native email/password authentication. No browser, callback,
        cookies, client secret or platform-store registration is involved.
        Use the fixed client matching the platform and the installation binding.
        Core preserves password policy, account state, rate limits and MFA.
        Login returns mfa_required=true with a five-minute, five-attempt challenge
        when enrolled; no credential is issued before verification. Native MFA
        challenges cannot be redeemed by browser MFA or another installation.
        Registration creates an unverified account and sends verification email;
        verification gates recovery, not access. A verification_delivery_failed
        response means the account exists; request another email and log in.
        Passwords and challenges are memory-only; never log or automatically retry.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/NativePasswordMFAVerifyRequest"
      responses:
        "200":
          description: Native authentication result. No Set-Cookie headers.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/NativeMobileTokenSet"
        "400":
          $ref: "#/components/responses/PasswordRejected"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "409":
          $ref: "#/components/responses/Conflict"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          description: Maintenance or verification_delivery_failed after account creation.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /api/v1/mobile/oauth/authorize:
    get:
      tags:
        - Authentication
      operationId: authorizeNativeMobileClient
      summary: Start or continue native mobile OAuth authorization
      description: |
        This is a system-browser-only Authorization Code + PKCE `S256`
        endpoint for a registered Assign Mobile public client. It never
        accepts a client secret. The browser may use its existing Assign
        session to complete the request, or it may sign in through the normal
        browser ceremony before returning here. The API then redirects only to
        the exact registered callback, which is the production universal/app
        link `https://api.assign.so/mobile/oauth/callback`.

        The mobile app must not follow this endpoint with an HTTP client, copy
        browser cookies, or treat the redirect as proof of API access. It
        receives a short-lived one-time code and exchanges that code at the
        token endpoint.
      security: []
      parameters:
        - name: client_id
          in: query
          required: true
          schema:
            $ref: "#/components/schemas/NativeMobileClientId"
        - name: redirect_uri
          in: query
          required: true
          schema:
            type: string
            format: uri
            maxLength: 2048
        - name: state
          in: query
          required: true
          schema:
            $ref: "#/components/schemas/OAuthState"
        - name: code_challenge
          in: query
          required: true
          schema:
            $ref: "#/components/schemas/PKCEChallenge"
        - name: code_challenge_method
          in: query
          required: true
          schema:
            type: string
            enum:
              - S256
        - name: installation_id
          in: query
          required: true
          schema:
            $ref: "#/components/schemas/NativeInstallationId"
        - name: platform
          in: query
          required: true
          schema:
            $ref: "#/components/schemas/NativePlatform"
        - name: device_label
          in: query
          required: true
          schema:
            $ref: "#/components/schemas/DeviceLabel"
        - name: prompt
          in: query
          required: false
          schema:
            type: string
            enum:
              - login
      responses:
        "200":
          description: Browser-rendered sign-in or authorization continuation page when a redirect is not immediately available.
          content:
            text/html:
              schema:
                type: string
        "302":
          description: Continues browser authentication or redirects to the exact registered native callback with an authorization code and the unchanged client state.
          headers:
            Location:
              description: Browser continuation or exact registered callback URI.
              schema:
                type: string
                format: uri
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
        "400":
          $ref: "#/components/responses/BadRequest"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/mobile/oauth/token:
    post:
      tags:
        - Authentication
      operationId: exchangeNativeMobileToken
      summary: Exchange a native authorization code or refresh token
      description: |
        Exchanges a native authorization code with its PKCE verifier, or the
        current rotating refresh token, for the next 15-minute opaque access
        token and refresh token. The request is bound to the registered public
        client and installation. Responses always carry `Cache-Control:
        no-store`; raw credentials must never be logged, cached, or placed in
        a URL. Reuse of an already-consumed refresh token revokes its entire
        credential family and requires browser reauthentication.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/NativeMobileTokenRequest"
      responses:
        "200":
          description: A rotated native access and refresh token pair.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/NativeMobileTokenSet"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/mobile/oauth/revoke:
    post:
      tags:
        - Authentication
      operationId: revokeCurrentNativeMobileCredential
      summary: Revoke the calling native mobile credential
      description: |
        Revokes only the credential represented by the native access token,
        including its refresh family and push registration. It is idempotent:
        a token that has already been revoked receives the same credential-free
        success response. The app clears local credentials and user-sensitive
        data after this request, whether it succeeds or is already invalid.
      security:
        - nativeAccessToken: []
      responses:
        "204":
          description: The current native credential is revoked
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/me/mobile-credentials:
    get:
      tags:
        - Account
      operationId: listNativeMobileCredentials
      summary: List active native mobile credentials
      description: Returns the caller's active mobile credentials across all Workspaces. Tokens, installation identifiers, and provider tokens are never returned.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/PageLimit"
      responses:
        "200":
          description: Active native credentials
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MobileCredentialList"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
    delete:
      tags:
        - Account
      operationId: revokeAllNativeMobileCredentials
      summary: Revoke every native mobile credential
      description: Revokes every active native credential and associated push registration. Recent browser authentication is required.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/CSRFToken"
      responses:
        "204":
          description: All native credentials were revoked
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/ReauthenticationRequired"
        "413":
          $ref: "#/components/responses/PayloadTooLarge"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/me/mobile-credentials/{credential_id}:
    delete:
      tags:
        - Account
      operationId: revokeNativeMobileCredential
      summary: Revoke one native mobile credential
      description: Immediately revokes one of the caller's native credentials and its push registration. A foreign or absent credential returns `404`.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/MobileCredentialId"
        - $ref: "#/components/parameters/CSRFToken"
      responses:
        "204":
          description: The credential was revoked
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/me/mobile-credentials/current/push-device:
    put:
      tags:
        - Account
      operationId: upsertCurrentNativePushDevice
      summary: Create or renew the calling credential's push-device registration
      description: |
        Binds one APNs or FCM delivery token to the calling native credential.
        The same token renews the registration; a replacement token atomically
        replaces the prior target. The provider token is accepted only for this
        request and is never returned.
      security:
        - nativeAccessToken: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PushDeviceUpsertRequest"
      responses:
        "200":
          description: Current registration state
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PushDeviceRegistration"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "409":
          $ref: "#/components/responses/Conflict"
        "413":
          $ref: "#/components/responses/PayloadTooLarge"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
    delete:
      tags:
        - Account
      operationId: deleteCurrentNativePushDevice
      summary: Remove the calling credential's push-device registration
      description: Idempotently removes the calling native credential's push target before logout, account switch, or notification-permission removal.
      security:
        - nativeAccessToken: []
      responses:
        "204":
          description: The push-device registration was removed
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/auth/session/workspace:
    put:
      tags:
        - Authentication
      operationId: switchSessionWorkspace
      summary: Switch the current session's Workspace
      description: |
        Revokes the current browser session and creates a new one scoped to
        a different Workspace the caller actively belongs to, rotating the
        bearer and CSRF secrets. There is no in-place reauthorization of the
        existing session onto a new scope.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/CSRFToken"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required:
                - workspace_id
              properties:
                workspace_id:
                  $ref: "#/components/schemas/Identifier"
      responses:
        "204":
          description: |
            The session now targets the requested Workspace. Two
            `Set-Cookie` fields replace `__Host-assign_session` and
            `__Host-assign_csrf` with freshly rotated values.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
            X-Request-ID:
              $ref: "#/components/headers/RequestId"
            X-Content-Type-Options:
              $ref: "#/components/headers/ContentTypeOptions"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/me:
    get:
      tags:
        - Account
      operationId: getCurrentAccount
      summary: Read the current user and Workspace
      description: |
        Returns the authenticated user and, when selected, the session's
        current Workspace, the caller's Actor within it, and their role. A
        newly registered account-only session omits the Workspace fields.
        Native bearer requests return the account-only projection and never
        inherit a browser session's selected Workspace. Upcoming interface_language_state
        uses bounded Accept-Language for this read, then the current credential hint when absent.
        Reading never persists a background-delivery hint.
      security:
        - browserSession: []
        - nativeAccessToken: []
      responses:
        "200":
          description: The current account.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
            ETag:
              $ref: "#/components/headers/ETag"
            X-Assign-Realtime-Cursor:
              description: Signed scope/User/session-bound replay custody; only when the requested canonical baseline succeeds.
              schema:
                type: string
                maxLength: 4096
            X-Assign-Realtime-Position:
              description: Decimal-string snapshot position; only with a successful baseline cursor.
              schema:
                type: string
                pattern: ^(0|[1-9][0-9]*)$
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Me"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
      parameters:
        - name: X-Assign-Realtime-Baseline
          in: header
          required: false
          description: Upcoming browser-session canonical read custody. The named private scope captures this response and a signed replay cursor in one snapshot. Successful responses include X-Assign-Realtime-Cursor and X-Assign-Realtime-Position. Apply all required retained state before acknowledging the corresponding socket checkpoint. Failed or incomplete baselines return no cursor.
          schema:
            type: string
            enum:
              - account
              - account_security
    patch:
      tags:
        - Account
      operationId: updateCurrentProfile
      summary: Update the current user's profile
      description: |
        Updates one or more editable current-account profile and interface
        preference fields. Email remains read-only here: changing it is a
        separate verification flow that must prove control of the new mailbox.

        The update is compare-and-swap. `If-Match` carries the user revision
        last observed by the client, and a stale value is refused with `409`
        rather than silently overwriting a concurrent edit.
      security:
        - browserSession: []
        - nativeAccessToken: []
      parameters:
        - $ref: "#/components/parameters/BrowserCSRFToken"
        - $ref: "#/components/parameters/IfMatch"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateProfileRequest"
      responses:
        "200":
          description: The updated account.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
            ETag:
              $ref: "#/components/headers/ETag"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Me"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "409":
          $ref: "#/components/responses/Conflict"
        "413":
          $ref: "#/components/responses/PayloadTooLarge"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/me/interface-language/session:
    put:
      tags:
        - Account
      operationId: reportInterfaceLanguage
      summary: Report current device language preferences
      description: |
        Upcoming. Report the ordered device languages on authenticated foreground entry or change.
        Core stores only the supported language hint for this browser session or native installation.
        This does not change the desired Account interface_language or regional/Voice settings.
        Use interface_language_state.hint_revision from GET /api/v1/me as If-Match, not the profile ETag.
        Repeating an unchanged effective hint does not advance its revision. A stale hint returns 409.
        Browser requests require CSRF. Native bearer requests use the authenticated installation.
      security:
        - browserSession: []
        - nativeAccessToken: []
      parameters:
        - $ref: "#/components/parameters/IfMatch"
        - name: X-CSRF-Token
          in: header
          required: false
          description: Required for browser sessions; native bearer requests do not use CSRF.
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/DeviceLanguageReport"
      responses:
        "200":
          description: Effective language and current session hint revision.
          headers:
            ETag:
              $ref: "#/components/headers/ETag"
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/InterfaceLanguageState"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "409":
          $ref: "#/components/responses/Conflict"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/me/email-change:
    post:
      tags:
        - Account
      operationId: beginCurrentEmailChange
      summary: Start a verified email-address change
      description: Requires recent authentication. The current address remains authoritative until the session-bound, single-use token delivered to the new inbox is consumed.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/CSRFToken"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required:
                - email
              properties:
                email:
                  type: string
                  format: email
                  maxLength: 320
      responses:
        "202":
          description: Verification delivery accepted
          content:
            application/json:
              schema:
                type: object
                required:
                  - status
                  - expires_at
                properties:
                  status:
                    type: string
                    enum:
                      - verification_sent
                  expires_at:
                    type: string
                    format: date-time
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "409":
          $ref: "#/components/responses/Conflict"
        "500":
          $ref: "#/components/responses/InternalServerError"
  /api/v1/me/email-change/verify:
    post:
      tags:
        - Account
      operationId: completeCurrentEmailChange
      summary: Verify and apply an email-address change
      description: Consumes the token only in the session that requested it, changes the verified address, invalidates other identity tokens, and revokes every other session.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/CSRFToken"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required:
                - token
              properties:
                token:
                  type: string
                  minLength: 1
                  maxLength: 512
      responses:
        "200":
          description: Email changed.
          content:
            application/json:
              schema:
                type: object
                required:
                  - email
                  - revision
                  - updated_at
                properties:
                  email:
                    type: string
                    format: email
                  revision:
                    type: integer
                    format: int64
                  updated_at:
                    type: string
                    format: date-time
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "409":
          $ref: "#/components/responses/Conflict"
        "500":
          $ref: "#/components/responses/InternalServerError"
  /api/v1/me/profile-picture:
    post:
      tags:
        - Account
      operationId: setCurrentProfilePicture
      summary: Associate a cropped S3-hosted profile picture
      security:
        - browserSession: []
        - nativeAccessToken: []
      parameters:
        - $ref: "#/components/parameters/BrowserCSRFToken"
        - $ref: "#/components/parameters/IfMatch"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required:
                - attachment_id
              properties:
                attachment_id:
                  $ref: "#/components/schemas/Identifier"
      responses:
        "200":
          description: Picture associated.
          headers:
            ETag:
              $ref: "#/components/headers/ETag"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProfilePictureMutation"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "409":
          $ref: "#/components/responses/Conflict"
    delete:
      tags:
        - Account
      operationId: clearCurrentProfilePicture
      summary: Remove the current profile picture and retire its stored object
      security:
        - browserSession: []
        - nativeAccessToken: []
      parameters:
        - $ref: "#/components/parameters/BrowserCSRFToken"
        - $ref: "#/components/parameters/IfMatch"
      responses:
        "200":
          description: Picture removed.
          headers:
            ETag:
              $ref: "#/components/headers/ETag"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProfilePictureMutation"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "409":
          $ref: "#/components/responses/Conflict"
  /api/v1/me/profile-picture/content:
    get:
      tags:
        - Account
      operationId: getCurrentProfilePictureContent
      summary: Redirect to a short-lived inline S3 URL for the current profile picture
      security:
        - browserSession: []
        - nativeAccessToken: []
      responses:
        "200":
          description: Image bytes after a client follows the redirect.
        "302":
          description: Short-lived object-storage redirect.
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
  /api/v1/me/api-tokens:
    get:
      tags:
        - Account
      operationId: listPersonalApiTokens
      summary: List personal API tokens for the current Workspace
      description: |
        Returns the current user's active, unrevoked personal API tokens for
        the Workspace selected by the browser session. Token secrets are never
        returned.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/PageLimit"
      responses:
        "200":
          description: Active personal API tokens
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PersonalApiTokenList"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
    post:
      tags:
        - Account
      operationId: createPersonalApiToken
      summary: Create a personal API token for the current Workspace
      description: |
        Creates an expiring, Workspace-bound token for non-interactive API and
        CLI use. The raw secret is returned exactly once in this response and
        must not be persisted by a browser or embedded in a URL. Recent
        authentication is required.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/CSRFToken"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PersonalApiTokenCreateRequest"
      responses:
        "201":
          description: Newly created token and its one-time secret
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PersonalApiTokenCreated"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/ReauthenticationRequired"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/me/current-work:
    get:
      tags:
        - Account
      operationId: getCurrentWork
      summary: Read the current user's account-global current task
      security:
        - browserSession: []
      responses:
        "200":
          description: Current-work state. `current` is null when no task is selected.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
            ETag:
              $ref: "#/components/headers/ETag"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CurrentWorkState"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalServerError"
    put:
      tags:
        - Account
      operationId: setCurrentTask
      summary: Select the current user's current task
      description: The task must be active, in progress, and assigned to the current user. `If-Match` prevents concurrent account-level selection changes from being overwritten.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/IfMatch"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SetCurrentTaskRequest"
      responses:
        "200":
          description: Updated current-work state.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
            ETag:
              $ref: "#/components/headers/ETag"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CurrentWorkState"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "409":
          $ref: "#/components/responses/Conflict"
        "500":
          $ref: "#/components/responses/InternalServerError"
    delete:
      tags:
        - Account
      operationId: clearCurrentTask
      summary: Clear the current user's current task
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/IfMatch"
      responses:
        "200":
          description: Cleared current-work state.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
            ETag:
              $ref: "#/components/headers/ETag"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CurrentWorkState"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "409":
          $ref: "#/components/responses/Conflict"
        "500":
          $ref: "#/components/responses/InternalServerError"
  /api/v1/me/current-work/candidates:
    get:
      tags:
        - Account
      operationId: listCurrentWorkCandidates
      summary: List tasks eligible to become the current task
      description: Returns up to 25 active in-progress tasks assigned to the current user across their Workspaces, excluding the selected task.
      security:
        - browserSession: []
      parameters:
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 25
            default: 25
      responses:
        "200":
          description: Eligible current-task candidates.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CurrentWorkCandidateList"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalServerError"
  /api/v1/me/api-tokens/{token_id}:
    delete:
      tags:
        - Account
      operationId: revokePersonalApiToken
      summary: Revoke a personal API token
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/PersonalApiTokenId"
        - $ref: "#/components/parameters/CSRFToken"
      responses:
        "204":
          description: The token was revoked
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/me/sessions:
    get:
      tags:
        - Account
      operationId: listCurrentUserSessions
      summary: List the current user's sessions
      description: |
        Returns the caller's own live sessions across every Workspace, newest
        first. Revoked and expired sessions are omitted.

        Sessions carry no IP address, user-agent, device name, or location.
        None is stored, and adding it would reverse the data-minimization
        decision in SECURITY-AUTH-001 that keeps network and client identity
        out of session records and session events. Sessions are distinguished
        by Workspace and last-seen time.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/PageLimit"
        - name: X-Assign-Realtime-Baseline
          in: header
          required: false
          description: Upcoming browser-session canonical read custody. The named private scope captures this response and a signed replay cursor in one snapshot. Successful responses include X-Assign-Realtime-Cursor and X-Assign-Realtime-Position. Apply all required retained state before acknowledging the corresponding socket checkpoint. Failed or incomplete baselines return no cursor.
          schema:
            type: string
            enum:
              - account_security
      responses:
        "200":
          description: A page of sessions.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
            X-Assign-Realtime-Cursor:
              description: Signed scope/User/session-bound replay custody; only when the requested canonical baseline succeeds.
              schema:
                type: string
                maxLength: 4096
            X-Assign-Realtime-Position:
              description: Decimal-string snapshot position; only with a successful baseline cursor.
              schema:
                type: string
                pattern: ^(0|[1-9][0-9]*)$
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SessionList"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
    delete:
      tags:
        - Account
      operationId: revokeOtherSessions
      summary: Sign out every other session
      description: |
        Revokes every session belonging to the caller **except the calling
        one**. Signing out the current session is `POST /api/v1/auth/logout`,
        which owns cookie expiry.

        Revocation is authoritative outside API process memory and is
        enforced immediately across every instance. One `session.revoked`
        event is recorded for each session whose state actually changes, in
        that session's own Workspace; if any event cannot be recorded, every
        revocation in the request rolls back. Repeating the call revokes
        nothing further and reports zero.

        Requires authentication within the last 15 minutes.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/CSRFToken"
      responses:
        "200":
          description: The number of sessions revoked by this request.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RevokedSessions"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/ReauthenticationRequired"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/me/mcp/grants:
    get:
      tags:
        - Account
      operationId: listCurrentUserMcpGrants
      summary: List connected MCP clients
      description: |
        Returns the caller's active MCP OAuth grants, newest first, including
        the reviewed client identity, scopes, authorized Workspaces, last use,
        and absolute expiry. Raw access and refresh tokens are never returned.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/PageLimit"
      responses:
        "200":
          description: A page of connected MCP clients.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/McpGrantList"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/me/mcp/grants/{grant_id}:
    delete:
      tags:
        - Account
      operationId: revokeCurrentUserMcpGrant
      summary: Disconnect an MCP client
      description: |
        Immediately revokes one MCP OAuth grant belonging to the caller.
        Access-token verification and refresh fail on every instance after the
        database transaction commits. A foreign or absent grant returns `404`.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/McpGrantId"
      responses:
        "204":
          description: The MCP grant is revoked.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/workspaces/{workspace_id}/ai-access-policy:
    get:
      tags:
        - Workspaces
      operationId: getWorkspaceAiAccessPolicy
      summary: Get the Workspace AI access policy
      description: |
        Returns the revisioned policy checked on every MCP authorization and
        Workspace tool request. An absent stored row defaults to enabled at
        revision zero.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
      responses:
        "200":
          description: The current AI access policy.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
            ETag:
              $ref: "#/components/headers/ETag"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WorkspaceAiAccessPolicy"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
    put:
      tags:
        - Workspaces
      operationId: updateWorkspaceAiAccessPolicy
      summary: Update the Workspace AI access policy
      description: |
        Enables or disables external AI access for the current Workspace.
        Disabling takes effect for existing MCP grants on their next request
        without revoking those grants in another Workspace.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/PolicyIfMatch"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/WorkspaceAiAccessPolicyUpdateRequest"
      responses:
        "200":
          description: The updated AI access policy.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
            ETag:
              $ref: "#/components/headers/ETag"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WorkspaceAiAccessPolicy"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/workspaces/{workspace_id}/mcp-service-credentials:
    get:
      tags:
        - Workspaces
      operationId: listWorkspaceMcpServiceCredentials
      summary: List Workspace MCP service credentials
      description: Lists at most 100 active, unexpired named service credentials. Raw credential values are never returned.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
      responses:
        "200":
          description: Active service credentials visible to a Workspace credential manager.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/McpServiceCredentialList"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
    post:
      tags:
        - Workspaces
      operationId: createWorkspaceMcpServiceCredential
      summary: Create a Workspace MCP service credential
      description: |
        Creates a non-human service actor and a Workspace-bound MCP credential.
        The raw credential is returned exactly once. An empty `project_ids`
        array permits every currently visible Project in the Workspace; a
        non-empty array narrows the credential to those Projects.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - $ref: "#/components/parameters/CSRFToken"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/McpServiceCredentialCreateRequest"
      responses:
        "201":
          description: The credential and its one-time raw token.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/McpServiceCredentialCreated"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/ReauthenticationRequired"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/workspaces/{workspace_id}/mcp-service-credentials/{credential_id}:
    delete:
      tags:
        - Workspaces
      operationId: revokeWorkspaceMcpServiceCredential
      summary: Revoke a Workspace MCP service credential
      description: Immediately refuses the credential on every instance while retaining its service actor for audit history.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - $ref: "#/components/parameters/McpServiceCredentialId"
        - $ref: "#/components/parameters/CSRFToken"
      responses:
        "204":
          description: The credential is revoked.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/me/sessions/{session_id}:
    delete:
      tags:
        - Account
      operationId: revokeCurrentUserSession
      summary: Revoke one of the current user's sessions
      description: |
        Revokes a single session belonging to the caller. Revoking the
        calling session is permitted, and the response then also expires the
        session and CSRF cookies.

        Revoking an already-revoked session answers `204`: the operation is
        idempotent, because a client retrying a lost response must not be
        told its device is still signed in. A session belonging to another
        user answers `404`, never `403`, so the endpoint cannot confirm that
        a session identifier exists.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/SessionId"
      responses:
        "204":
          description: The session is revoked.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/me/identities:
    get:
      tags:
        - Account
      operationId: listLinkedIdentities
      summary: List linked external identities
      description: |
        Returns the caller's linked provider identities. The provider subject
        is never returned: it is a stable cross-service correlator with no
        client use.
      security:
        - browserSession: []
      responses:
        "200":
          description: The caller's linked identities.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
            X-Assign-Realtime-Cursor:
              description: Signed scope/User/session-bound replay custody; only when the requested canonical baseline succeeds.
              schema:
                type: string
                maxLength: 4096
            X-Assign-Realtime-Position:
              description: Decimal-string snapshot position; only with a successful baseline cursor.
              schema:
                type: string
                pattern: ^(0|[1-9][0-9]*)$
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LinkedIdentityList"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
      parameters:
        - name: X-Assign-Realtime-Baseline
          in: header
          required: false
          description: Upcoming browser-session canonical read custody. The named private scope captures this response and a signed replay cursor in one snapshot. Successful responses include X-Assign-Realtime-Cursor and X-Assign-Realtime-Position. Apply all required retained state before acknowledging the corresponding socket checkpoint. Failed or incomplete baselines return no cursor.
          schema:
            type: string
            enum:
              - account_security
    post:
      tags:
        - Account
      operationId: startIdentityLink
      summary: Start linking an external identity
      description: |
        Starts a server-side link ceremony and returns the provider
        authorization URL to send the user to. It does **not** accept a
        provider token or identity assertion: the backend owns the
        account-linking decision, and a client-supplied assertion must never
        determine who owns an account.

        The existing provider callback completes the ceremony. A ceremony is
        marked as a link and is claimed by a single conditional update, so a
        link ceremony can never be redeemed as a sign-in and a sign-in
        ceremony can never be redeemed as an explicit account-settings link.

        The ceremony is bound to the already authenticated target account and
        does not choose that target from the provider email. An identity
        already belonging to another account is refused `409` and is never
        transferred. Linking an identity already linked to the caller is
        idempotent. Separately, ordinary provider sign-in may attach a
        previously unseen identity when the provider verifies the same
        normalized account email.

        Requires authentication within the last 15 minutes.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/CSRFToken"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/StartIdentityLinkRequest"
      responses:
        "201":
          description: The link ceremony has started.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IdentityLinkCeremony"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/ReauthenticationRequired"
        "409":
          $ref: "#/components/responses/Conflict"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/me/identities/{identity_id}:
    delete:
      tags:
        - Account
      operationId: unlinkIdentity
      summary: Unlink an external identity
      description: |
        Removes a linked provider identity.

        Refused with `409 last_sign_in_method` when removing it would leave
        the account with no usable way to sign in, counting a password
        credential, each registered passkey, and each remaining identity. An
        identity belonging to another user answers `404`. Repeat deletion of
        an already-unlinked identity answers `204`.

        Requires authentication within the last 15 minutes.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/IdentityId"
      responses:
        "204":
          description: The identity is unlinked.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/ReauthenticationRequired"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/workspaces/bootstrap:
    post:
      tags:
        - Workspaces
      operationId: bootstrapWorkspace
      summary: Create the caller's first Workspace
      description: |
        Creates the first Workspace, owner membership, and Actor for a signed-in
        account that has no active Workspace memberships. The caller selects
        both the visible name and canonical URL segment.

        Success rotates the account-only session into the new Workspace and
        replaces both browser-session cookies. Concurrent requests are
        serialized so at most one first Workspace is created for an account.
        A caller that already has a Workspace receives `409` and must use the
        normal session-switch operation instead.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/CSRFToken"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/BootstrapWorkspaceRequest"
      responses:
        "201":
          description: |
            The first Workspace was created and selected. Two `Set-Cookie`
            fields replace `__Host-assign_session` and `__Host-assign_csrf`.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
            X-Request-ID:
              $ref: "#/components/headers/RequestId"
            X-Content-Type-Options:
              $ref: "#/components/headers/ContentTypeOptions"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Workspace"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "409":
          $ref: "#/components/responses/Conflict"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/mobile/workspace-creation-options:
    get:
      tags:
        - Workspaces
      operationId: getNativeWorkspaceCreationOptions
      summary: Get native account Workspace-creation options
      description: |
        Requires a first-party native bearer; browser cookies are not accepted.
        Paid choices are informational until native paid-checkout admission is qualified.
        Returns the signed-in human account's Free Workspace claim state and
        currently purchasable paid choices. Membership and the selected
        Workspace role do not affect this account-level eligibility. The
        Paid choices are present only when the configured provider and every
        required immutable local catalog mapping are available.
      security:
        - nativeAccessToken: []
      responses:
        "200":
          description: Bounded account creation options.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WorkspaceCreationOptions"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/mobile/workspaces:
    post:
      tags:
        - Workspaces
      operationId: createNativeWorkspace
      summary: Create a Free Workspace with a native account
      description: |
        Requires a first-party native bearer; browser cookies are not accepted.
        Creation does not select a Workspace or issue browser cookies.
        Claims and creates the signed-in human account's one active Free
        Workspace, provisioning the Workspace, owner membership, Actor,
        default workflow, claim, and events in one transaction. Membership
        and role in the selected Workspace do not grant or deny this
        account-level command.

        The optional `slug` selects the canonical URL segment. When omitted,
        Core generates a collision-safe value from the name.

        Creation does **not** switch the calling session onto the new
        Workspace. Changing scope is `PUT /api/v1/auth/session/workspace`,
        which always rotates the session and CSRF secrets; doing it as a side
        effect here would rotate them invisibly.
      security:
        - nativeAccessToken: []
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateWorkspaceRequest"
      responses:
        "201":
          description: The created Workspace.
          headers:
            ETag:
              $ref: "#/components/headers/ETag"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Workspace"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "409":
          $ref: "#/components/responses/Conflict"
        "413":
          $ref: "#/components/responses/PayloadTooLarge"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/workspace-creation-options:
    get:
      tags:
        - Workspaces
      operationId: getWorkspaceCreationOptions
      summary: Get account Workspace-creation options
      description: |
        Returns the signed-in human account's Free Workspace claim state and
        currently purchasable paid choices. Membership and the selected
        Workspace role do not affect this account-level eligibility. The
        Paid choices are present only when the configured provider and every
        required immutable local catalog mapping are available.
      security:
        - browserSession: []
      responses:
        "200":
          description: Bounded account creation options.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WorkspaceCreationOptions"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/workspace-checkouts:
    post:
      tags:
        - Billing
      operationId: createWorkspaceCheckout
      summary: Start paid Workspace creation
      description: |
        Creates or replays an expiring account-level pending checkout and
        returns a provider-hosted URL. No usable Workspace exists yet. Only a
        signature-verified provider event followed by subscription
        reconciliation may activate the Workspace; browser return is
        navigation only.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateWorkspaceCheckoutRequest"
      responses:
        "201":
          description: Pending paid Workspace checkout.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WorkspaceCheckout"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "409":
          $ref: "#/components/responses/Conflict"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/workspace-checkouts/{checkout_id}:
    get:
      tags:
        - Billing
      operationId: getWorkspaceCheckout
      summary: Read paid Workspace creation state
      description: Returns only the current account's bounded checkout state. A foreign identifier is indistinguishable from an absent one.
      security:
        - browserSession: []
      parameters:
        - name: checkout_id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/Identifier"
        - name: X-Assign-Realtime-Baseline
          in: header
          required: false
          description: Upcoming browser-session canonical Account read. Captures the checkout response and a signed Account replay cursor in one read-only snapshot. Apply the response before acknowledging the socket checkpoint. An ordinary read may expire a pending checkout; a canonical baseline never mutates it.
          schema:
            type: string
            enum:
              - account
      responses:
        "200":
          description: Current pending or terminal checkout state.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
            X-Assign-Realtime-Cursor:
              description: Signed Account replay cursor on a successful canonical baseline.
              schema:
                type: string
            X-Assign-Realtime-Position:
              description: Account source position captured with the canonical baseline.
              schema:
                type: integer
                format: int64
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WorkspaceCheckout"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/workspaces:
    get:
      tags:
        - Account
      operationId: listCurrentUserWorkspaces
      summary: List current-user Workspaces
      description: |
        Returns one page of the caller's active Workspace memberships,
        ordered by Workspace name.
      security:
        - browserSession: []
        - nativeAccessToken: []
      parameters:
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/PageLimit"
      responses:
        "200":
          description: A page of Workspace memberships.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WorkspaceSummaryList"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
    post:
      tags:
        - Workspaces
      operationId: createWorkspace
      summary: Create a Workspace
      description: |
        Claims and creates the signed-in human account's one active Free
        Workspace, provisioning the Workspace, owner membership, Actor,
        default workflow, claim, and events in one transaction. Membership
        and role in the selected Workspace do not grant or deny this
        account-level command.

        The optional `slug` selects the canonical URL segment. When omitted,
        Core generates a collision-safe value from the name.

        Creation does **not** switch the calling session onto the new
        Workspace. Changing scope is `PUT /api/v1/auth/session/workspace`,
        which always rotates the session and CSRF secrets; doing it as a side
        effect here would rotate them invisibly.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateWorkspaceRequest"
      responses:
        "201":
          description: The created Workspace.
          headers:
            ETag:
              $ref: "#/components/headers/ETag"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Workspace"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "409":
          $ref: "#/components/responses/Conflict"
        "413":
          $ref: "#/components/responses/PayloadTooLarge"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/workspaces/{workspace_id}/statuses:
    get:
      tags:
        - Workspaces
      operationId: listWorkspaceStatuses
      summary: List Workspace-wide workflow Statuses
      description: |
        Returns a cursor-bounded, workflow-ordered page of Statuses whose
        `project_id` is null and which therefore apply to every Project in
        the Workspace. Active members with `work:read` may read the catalog.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/PageLimit"
        - name: include_archived
          in: query
          description: Include archived Statuses; omitted or false returns active Statuses only.
          schema:
            type: boolean
            default: false
      responses:
        "200":
          description: A page of Workspace-wide workflow Statuses.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/StatusList"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
    post:
      tags:
        - Workspaces
      operationId: createWorkspaceStatus
      summary: Create a Workspace-wide workflow Status
      description: |
        Appends a Status that applies to every Project in the Workspace.
        This requires `workspace:manage`: unlike a Project-local Status, the
        new catalog entry changes how all Workspace work is categorized.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/StatusCreateRequest"
      responses:
        "201":
          description: The created Workspace-wide Status.
          headers:
            ETag:
              $ref: "#/components/headers/ETag"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Status"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/lifecycle-behaviors:
    get:
      tags:
        - Workspaces
      operationId: listLifecycleBehaviors
      summary: List the typed lifecycle behavior registry
      description: Returns every behavior key a stage can enable, the entity kinds it applies to and the domain consumer that owns its effect. The list is static for a server version and safe to cache briefly.
      security:
        - browserSession: []
      responses:
        "200":
          description: The behavior registry.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LifecycleBehaviorList"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalServerError"
  /api/v1/workspaces/{workspace_id}/lifecycle-completion-targets:
    get:
      tags:
        - Workspaces
      operationId: resolveLifecycleCompletionTarget
      summary: Resolve the status that completes an entity of a kind
      description: |
        Requires Workspace read access. Completion is never inferred from
        labels. With one completing status the response names it. With several
        and no `target_status_id` the response is `409 completion_target_required`
        listing the choices, so the caller must name one. An explicit target
        that does not count as completed is `422 completion_target_invalid`;
        a kind without any completing status is `422 completion_target_unavailable`.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - name: kind
          in: query
          required: true
          schema:
            $ref: "#/components/schemas/LifecycleKind"
        - name: definition_id
          in: query
          required: false
          schema:
            $ref: "#/components/schemas/Identifier"
        - name: target_status_id
          in: query
          required: false
          schema:
            $ref: "#/components/schemas/Identifier"
      responses:
        "200":
          description: The completing status.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LifecycleCompletionTarget"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          description: Several statuses can complete the entity; name one.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LifecycleCompletionChoices"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"
        "500":
          $ref: "#/components/responses/InternalServerError"
  /api/v1/workspaces/{workspace_id}/lifecycle-definitions:
    get:
      tags:
        - Workspaces
      operationId: listLifecycleDefinitions
      summary: List lifecycle definitions with stages, policies and statuses
      description: |
        Requires work:read. Returns the Workspace's definitions (optionally one
        kind) with every stage's complete typed behaviors and bound statuses in
        presentation order, so selectors and settings render from one read.
        The catalog is bounded by the lifecycle limits (20 definitions per kind,
        100 stages per definition, 500 statuses per definition), so it is not
        paged. Behaviors that do not apply to the kind are absent.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - name: kind
          in: query
          schema:
            $ref: "#/components/schemas/LifecycleKind"
        - name: include_archived
          in: query
          schema:
            type: boolean
            default: false
      responses:
        "200":
          description: The Workspace lifecycle catalog.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LifecycleDefinitionList"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"
  /api/v1/workspaces/{workspace_id}/lifecycle-definitions/{definition_id}/mapping-previews:
    post:
      tags:
        - Workspaces
      operationId: previewLifecycleMapping
      summary: Preview moving statuses of a lifecycle definition
      description: |
        Requires Workspace management authority. Validates that every source
        status has exactly one same-kind, same-Workspace, active destination
        that is not itself retired, and returns bounded per-status impact counts
        (capped at 10,000, `truncated` when reached) and the behavior keys whose
        value would change. `If-Match` binds the definition revision; a mismatch
        is `409 revision_conflict`. Nothing is written. Pass the returned
        `destination_revisions` and `source_revision` back to apply.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - $ref: "#/components/parameters/LifecycleDefinitionId"
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/IfMatch"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/LifecycleMappingRequest"
      responses:
        "200":
          description: The impact preview.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LifecycleMappingPreview"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"
        "500":
          $ref: "#/components/responses/InternalServerError"
  /api/v1/workspaces/{workspace_id}/lifecycle-definitions/{definition_id}/mapping-applications:
    post:
      tags:
        - Workspaces
      operationId: applyLifecycleMapping
      summary: Apply a previewed status mapping as a durable migration job
      description: |
        Requires Workspace management authority and an `Idempotency-Key`; a
        repeat with the same key and mapping returns the original job and a
        different mapping is `409 idempotency_key_conflict`. The request is
        re-validated against `If-Match` and `destination_revisions`
        (`409 lifecycle_preview_stale` or `revision_conflict`), and material
        behavior changes or a default replacement need `acknowledged: true`
        (`409 lifecycle_acknowledgement_required`). Scopes within the
        1,000-entity synchronous limit are moved before the response
        (`200`, status `completed`, or `202` when a retry is pending); larger
        scopes return `202` and a background worker moves bounded pages. A
        Workspace has one applying job (`409 lifecycle_migration_pending`).
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - $ref: "#/components/parameters/LifecycleDefinitionId"
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/IdempotencyKey"
        - $ref: "#/components/parameters/IfMatch"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/LifecycleMappingRequest"
      responses:
        "200":
          description: The migration completed inline.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LifecycleMigrationJob"
        "202":
          description: The migration is accepted and pending or retrying.
          headers:
            Location:
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LifecycleMigrationJob"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/workspaces/{workspace_id}/lifecycle-migrations/{job_id}:
    get:
      tags:
        - Workspaces
      operationId: getLifecycleMigration
      summary: Read a lifecycle migration job
      description: Requires Workspace management authority. Returns pending, processing, retry, completed or failed state with progress and a safe error code.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - $ref: "#/components/parameters/LifecycleMigrationJobId"
      responses:
        "200":
          description: The job.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LifecycleMigrationJob"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"
  /api/v1/workspaces/{workspace_id}/lifecycle-migrations/{job_id}/retry:
    post:
      tags:
        - Workspaces
      operationId: retryLifecycleMigration
      summary: Resume a failed lifecycle migration job
      description: Requires Workspace management authority. Makes a `failed` job runnable again, keeping pages already moved; any other state is `404 not_found`.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - $ref: "#/components/parameters/LifecycleMigrationJobId"
        - $ref: "#/components/parameters/CSRFToken"
      responses:
        "202":
          description: The job is pending again.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LifecycleMigrationJob"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "500":
          $ref: "#/components/responses/InternalServerError"
  /api/v1/workspaces/{workspace_id}/milestone-lifecycle:
    get:
      tags:
        - Workspaces
      operationId: getWorkspaceMilestoneLifecycle
      summary: Read configured Milestone stages
      description: Requires work:read. Returns at most 100 retained stages in presentation order. A Workspace without a saved catalog returns the four default stages at revision 1.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
      responses:
        "200":
          description: Ordered Milestone catalog
          headers:
            ETag:
              $ref: "#/components/headers/ETag"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MilestoneLifecycle"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
    put:
      tags:
        - Workspaces
      operationId: updateWorkspaceMilestoneLifecycle
      summary: Configure Milestone stages
      description: Requires workspace:manage. Atomically replaces the bounded catalog under If-Match. Every semantic category retains active coverage. Referenced IDs and categories cannot be removed or changed. Archiving an in-use stage requires a same-category active replacement; at most 1000 active Milestones are moved atomically. Archived Milestones retain historical selections. Conflicts include revision_conflict, milestone_stage_in_use and lifecycle_migration_too_large. Names are trimmed; no transition graph is introduced.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/IdempotencyKey"
        - $ref: "#/components/parameters/IfMatch"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/MilestoneLifecycleUpdateRequest"
      responses:
        "200":
          description: Updated Milestone catalog
          headers:
            ETag:
              $ref: "#/components/headers/ETag"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MilestoneLifecycle"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/workspaces/{workspace_id}/project-lifecycle:
    get:
      tags:
        - Projects
      operationId: getWorkspaceProjectLifecyclePolicy
      summary: Read the optional Project lifecycle policy
      description: Returns the revisioned Workspace policy. The durable default is disabled at revision 1.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
      responses:
        "200":
          description: Project lifecycle policy
          headers:
            ETag:
              $ref: "#/components/headers/ETag"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProjectLifecyclePolicy"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
    patch:
      tags:
        - Projects
      operationId: updateWorkspaceProjectLifecyclePolicy
      summary: Enable or disable Project lifecycle states
      description: |
        Requires `workspace:manage`. Disabling preserves state definitions and
        Project selections, but all catalog and Project-state mutations are
        refused until the capability is enabled again.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/IdempotencyKey"
        - $ref: "#/components/parameters/IfMatch"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ProjectLifecyclePolicyUpdateRequest"
      responses:
        "200":
          description: Updated policy
          headers:
            ETag:
              $ref: "#/components/headers/ETag"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProjectLifecyclePolicy"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/workspaces/{workspace_id}/project-states:
    get:
      tags:
        - Projects
      operationId: listWorkspaceProjectStates
      summary: List Workspace Project states
      description: Returns a cursor-bounded ordered page. Active members may read retained definitions even while the optional capability is disabled.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/PageLimit"
        - name: include_archived
          in: query
          schema:
            type: boolean
            default: false
      responses:
        "200":
          description: Project state page
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProjectStateList"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
    post:
      tags:
        - Projects
      operationId: createWorkspaceProjectState
      summary: Create a Project state
      description: Requires `workspace:manage`, an enabled lifecycle policy, and fewer than 100 retained definitions.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ProjectStateCreateRequest"
      responses:
        "201":
          description: Created Project state
          headers:
            ETag:
              $ref: "#/components/headers/ETag"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProjectState"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "409":
          $ref: "#/components/responses/Conflict"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/project-states/{state_id}:
    patch:
      tags:
        - Projects
      operationId: updateProjectState
      summary: Rename or restore a Project state
      description: "Changes exactly one field. Restore uses `archived: false`; the capability must be enabled."
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/ProjectStateId"
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/IdempotencyKey"
        - $ref: "#/components/parameters/IfMatch"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ProjectStateUpdateRequest"
      responses:
        "200":
          description: Updated Project state
          headers:
            ETag:
              $ref: "#/components/headers/ETag"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProjectState"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
    delete:
      tags:
        - Projects
      operationId: archiveProjectState
      summary: Archive an unused Project state
      description: Refuses an in-use state with `409 project_state_in_use`; use the replacement command for that case.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/ProjectStateId"
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/IdempotencyKey"
        - $ref: "#/components/parameters/IfMatch"
      responses:
        "204":
          description: Project state archived
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/project-states/{state_id}/archive-and-replace:
    post:
      tags:
        - Projects
      operationId: archiveAndReplaceProjectState
      summary: Replace an in-use Project state and archive it
      description: Atomically moves active Projects to a different active same-Workspace state and archives the source.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/ProjectStateId"
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/IdempotencyKey"
        - $ref: "#/components/parameters/IfMatch"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ProjectStateArchiveAndReplaceRequest"
      responses:
        "204":
          description: Projects replaced and source state archived
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/project-states/{state_id}/move:
    post:
      tags:
        - Projects
      operationId: moveProjectState
      summary: Move a Project state
      description: Reorders the bounded active catalog using immediate neighbour anchors.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/ProjectStateId"
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ProjectStateMoveRequest"
      responses:
        "200":
          description: Moved Project state
          headers:
            ETag:
              $ref: "#/components/headers/ETag"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProjectState"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/workspaces/{workspace_id}/members:
    get:
      tags:
        - Account
      operationId: listWorkspaceMembers
      summary: List Workspace members
      description: |
        Returns one page of the Workspace's active memberships, for assignee
        pickers and mention candidates. Callers with `members:manage` may set
        `include_suspended=true` for the member-settings governance view;
        suspended memberships remain excluded from ordinary pickers.
        `workspace_id` must match the
        caller's current session Workspace; any other value reports the same
        not-found response as an absent Workspace.
      security:
        - browserSession: []
        - nativeAccessToken: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/PageLimit"
        - name: include_suspended
          in: query
          required: false
          schema:
            type: boolean
            default: false
          description: Include suspended memberships. Requires `members:manage` when true.
      responses:
        "200":
          description: A page of Workspace members.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MemberList"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
    post:
      tags:
        - Workspaces
      operationId: addWorkspaceMember
      summary: Restore a former member
      description: |
        Reactivates the membership of somebody who was removed or who left.
        It takes a `user_id` an administrator has already seen in the member
        list, and it is **not** a path for net-new people.

        There is deliberately no address input. An endpoint that resolved an
        arbitrary email to an account would answer, for any caller who can
        manage members, whether that address is registered with Assign.
        Net-new people join through invitations, which prove control of the
        mailbox instead.

        Reactivating an already-active membership is idempotent.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/WorkspaceId"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AddMemberRequest"
      responses:
        "200":
          description: The reactivated membership.
          headers:
            ETag:
              $ref: "#/components/headers/ETag"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Membership"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "413":
          $ref: "#/components/responses/PayloadTooLarge"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/workspaces/{workspace_id}:
    get:
      tags:
        - Workspaces
      operationId: getWorkspace
      summary: Read a Workspace
      description: |
        Returns the Workspace the caller's session is scoped to. Any active
        member may read it.

        An archived Workspace is visible only to an owner, so un-archiving
        stays discoverable; every other member sees the same not-found
        response an absent Workspace produces.
      security:
        - browserSession: []
        - nativeAccessToken: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
      responses:
        "200":
          description: The Workspace.
          headers:
            ETag:
              $ref: "#/components/headers/ETag"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Workspace"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
    patch:
      tags:
        - Workspaces
      operationId: updateWorkspace
      summary: Rename, change the URL, or restore a Workspace
      description: |
        Changes exactly one of `name`, `slug`, or `archived`. Renaming and URL
        changes require the `workspace:manage` capability; `archived: false`
        restores an archived Workspace and is the only mutation an archived
        Workspace accepts.

        `archived: true` is rejected here — archiving is
        `DELETE /api/v1/workspaces/{workspace_id}`, which additionally
        requires an owner and recent authentication.

        `slug` is the canonical Workspace URL segment. It must be unique and
        use lowercase letters, numbers, and interior hyphens only.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/WorkspaceId"
        - $ref: "#/components/parameters/IfMatch"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateWorkspaceRequest"
      responses:
        "200":
          description: The updated Workspace.
          headers:
            ETag:
              $ref: "#/components/headers/ETag"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Workspace"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "413":
          $ref: "#/components/responses/PayloadTooLarge"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
    delete:
      tags:
        - Workspaces
      operationId: archiveWorkspace
      summary: Archive a Workspace
      description: |
        Archives the Workspace. Requires an **owner** and authentication
        within the last 15 minutes, so a borrowed session cannot pull a
        Workspace out from under its members.

        **No data is deleted.** Every session scoped to the Workspace is
        revoked in the same transaction, the Workspace disappears from the
        default Workspace list, and every Workspace-scoped route answers
        not-found — except an owner's read and the restoring `PATCH`.
        Archival is therefore a real access boundary, not a display flag, and
        it is reversible.

        A caller's last active Workspace cannot be archived. Account-only
        sessions are reserved for first-Workspace onboarding, so an existing
        scoped account cannot return to that state by archiving its Workspace.
        That case answers `409 last_workspace`.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/WorkspaceId"
        - $ref: "#/components/parameters/IfMatch"
        - $ref: "#/components/parameters/IdempotencyKey"
      responses:
        "204":
          description: The Workspace is archived.
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/ReauthenticationRequired"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/workspaces/{workspace_id}/members/{member_id}:
    patch:
      tags:
        - Workspaces
      operationId: updateWorkspaceMembership
      summary: Change a member's role or state
      description: |
        Changes `role`, `state`, or both. An omitted field keeps its current
        value. `state` moves between `active` and `suspended`; suspending a
        member revokes their sessions in this Workspace immediately.
        Promotion to `owner` is permitted.

        Demoting, suspending, or otherwise removing the **last active owner**
        is refused `409 last_owner`. A caller may not change their own
        membership: self-demotion and self-suspension have no legitimate use
        and both produce a lockout the caller cannot then undo, so they answer
        `422 self_membership_mutation`.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/WorkspaceId"
        - $ref: "#/components/parameters/MemberId"
        - $ref: "#/components/parameters/IfMatch"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateMembershipRequest"
      responses:
        "200":
          description: The updated membership.
          headers:
            ETag:
              $ref: "#/components/headers/ETag"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Membership"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
    delete:
      tags:
        - Workspaces
      operationId: removeWorkspaceMember
      summary: Remove a member
      description: |
        Ends the membership. The row is retained in a `removed` state and the
        member's **Actor is retained**, so past Task assignments, comments,
        and events stay attributable to a real identity.

        The member's sessions in this Workspace are revoked in the same
        transaction, so access ends immediately rather than at next expiry.
        Removing the last active owner is refused `409 last_owner`, and a
        caller may not remove themselves. Repeat removal answers `204`.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/WorkspaceId"
        - $ref: "#/components/parameters/MemberId"
        - $ref: "#/components/parameters/IdempotencyKey"
      responses:
        "204":
          description: The membership is removed.
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/workspaces/{workspace_id}/invitations:
    get:
      tags:
        - Workspaces
      operationId: listWorkspaceInvitations
      summary: List invitations
      description: |
        Returns one newest-first page of invitations in every state —
        pending, accepted, expired, and revoked. Neither the token nor its
        hash is ever returned; the token exists in exactly one response, the
        one that created it.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/PageLimit"
      responses:
        "200":
          description: A page of invitations.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/InvitationList"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
    post:
      tags:
        - Workspaces
      operationId: createWorkspaceInvitation
      summary: Invite someone to the Workspace
      description: |
        Creates an invitation and returns the plain token **once**. Only a
        hash is stored, so the token cannot be recovered later and a database
        disclosure is not replayable into Workspace access. Repeating the
        request with the same `Idempotency-Key` replays the original
        response, token included, which is how a client that lost the first
        response recovers it.

        `role` may be `admin`, `member`, or `viewer` — **not `owner`**.
        Ownership carries the archive power and the last-owner invariant, and
        is conferred by a deliberate membership change on somebody already
        present rather than by whoever opens a link in a mailbox.

        A second unexpired pending invitation for the same address is refused
        `409 invitation_pending` rather than silently reissued, so the link
        the recipient already received stays the live one. An address that
        already belongs to an active member is refused `409 already_member`,
        which discloses nothing an administrator cannot read from the member
        list. If the configured email provider cannot accept the invitation,
        the transaction rolls back and the endpoint returns retryable
        `503 delivery_unavailable`.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/WorkspaceId"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateInvitationRequest"
      responses:
        "201":
          description: The invitation and its one-time token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CreatedInvitation"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "413":
          $ref: "#/components/responses/PayloadTooLarge"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/WorkspaceInvitationUnavailable"
  /api/v1/workspaces/{workspace_id}/invitations/{invitation_id}:
    delete:
      tags:
        - Workspaces
      operationId: revokeWorkspaceInvitation
      summary: Revoke an invitation
      description: |
        Withdraws a pending invitation, so its link no longer opens the
        Workspace. Repeating it answers `204`.

        Revoking an **accepted** invitation is refused
        `409 invitation_not_pending`: acceptance is not undone by revoking the
        link that produced it, and removing the resulting member is a
        separate, visible act.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/WorkspaceId"
        - $ref: "#/components/parameters/InvitationId"
        - $ref: "#/components/parameters/IdempotencyKey"
      responses:
        "204":
          description: The invitation is revoked.
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/workspaces/{workspace_id}/invitations/{invitation_id}/resend:
    post:
      tags:
        - Workspaces
      operationId: resendWorkspaceInvitation
      summary: Resend an invitation
      description: |
        Replaces an unaccepted invitation with a fresh seven-day invitation
        and sends the new link to the same normalized address with the same
        role. The previous link stops working only when delivery of the new
        message succeeds and the transaction commits. A delivery failure
        returns `503 delivery_unavailable` and leaves the previous pending
        invitation usable.

        Accepted, expired, and explicitly revoked invitations cannot be resent and
        return `409 invitation_not_pending`. Repeating the request with the
        same `Idempotency-Key` replays the same successful response without
        sending another message.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/WorkspaceId"
        - $ref: "#/components/parameters/InvitationId"
        - $ref: "#/components/parameters/IdempotencyKey"
      responses:
        "201":
          description: The replacement invitation and its one-time token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CreatedInvitation"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/WorkspaceInvitationUnavailable"
  /api/v1/invitations/accept:
    post:
      tags:
        - Workspaces
      operationId: acceptInvitation
      summary: Accept an invitation
      description: |
        Redeems an invitation token and joins the Workspace. Requires a
        signed-in session: the invitation's address must equal the caller's
        stored address exactly after normalization, so a forwarded message
        cannot transfer the grant. A mismatch answers
        `403 invitation_recipient_mismatch`. A live invitation proves control
        of the matching mailbox: acceptance verifies that address atomically
        with membership creation, without another verification email.

        Idempotency is account-scoped because the caller may have an
        authenticated account-only session and no target-Workspace membership
        before this operation succeeds.

        An expired invitation answers `410 invitation_expired`. A revoked or
        already-accepted one answers `409 invitation_not_pending`. An unknown
        token answers `404` — the same answer a swept expired token gives, so
        the endpoint is not a token oracle.

        Acceptance does **not** switch the calling session onto the joined
        Workspace; that is `PUT /api/v1/auth/session/workspace`.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AcceptInvitationRequest"
      responses:
        "200":
          description: The Workspace the caller joined.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Workspace"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "410":
          $ref: "#/components/responses/Gone"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/workspaces/{workspace_id}/search:
    get:
      tags:
        - Search
      operationId: searchWorkspace
      summary: Search Workspace content
      description: |
        Returns a bounded, cursor-paginated page of authorized Project, Task,
        Document, Comment, or active Person matches from exactly one Workspace. Exact
        identifiers and titles take precedence, followed by title prefixes,
        weighted lexical relevance, typo tolerance, and recency. The opaque
        cursor preserves that deterministic ordering. Plain-text snippets are
        bounded and stripped of control characters; results never expose a raw
        engine score or exact total. Archived resources are excluded unless
        explicitly included; trashed, purged, and deleted resources remain excluded.
        Work is rate-limited and query-time bounded; overload returns a
        retryable `503 search_unavailable` with `Retry-After`.
        Native bearer access is limited to title and identifier matches for an
        explicit resource_type of project or task, excludes archived resources,
        and returns empty snippets. Other types and omitted/duplicate type filters
        return 400 for native callers. Browser search retains its full corpus.
      security:
        - browserSession: []
        - nativeAccessToken: []
        - bearerToken: []
        - developerAccessToken: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - name: q
          in: query
          required: true
          description: Terms or quoted phrases. Leading and trailing whitespace is ignored.
          schema:
            type: string
            minLength: 2
            maxLength: 200
        - name: project_id
          in: query
          required: false
          description: Restrict matches to this Project. An inaccessible Project produces no results rather than confirming it exists.
          schema:
            $ref: "#/components/schemas/Identifier"
        - name: resource_type
          in: query
          required: false
          description: Restrict matches to one indexed resource type.
          schema:
            $ref: "#/components/schemas/SearchResourceType"
        - name: include_archived
          in: query
          required: false
          description: Include archived resources. Resolved Tasks are included regardless because resolution is not archival.
          schema:
            type: boolean
            default: false
        - $ref: "#/components/parameters/Cursor"
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 50
            default: 20
      responses:
        "200":
          description: A page of authorized Workspace content matches.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SearchResultList"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/workspaces/{workspace_id}/reference-options:
    get:
      tags:
        - Documents
      operationId: listWorkspaceReferenceOptions
      summary: Query bounded editor reference and mention candidates
      description: |
        Returns grouped, permission-filtered candidates for Assign editor
        mentions and references. Matching is prefix-and-word based across
        active member display names, active Workspace Agent names, unarchived
        Document titles, unarchived Project names and keys, and unarchived Task
        titles and ticket references. Each requested type is independently limited; no cursor
        or total is exposed. A blank query returns an empty result.
      security:
        - browserSession: []
        - bearerToken: []
        - developerAccessToken: []
        - nativeAccessToken: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - name: q
          in: query
          required: true
          description: Typeahead text. Leading and trailing whitespace is ignored; blank text returns no candidates.
          schema:
            type: string
            minLength: 0
            maxLength: 100
        - name: types
          in: query
          required: false
          description: Repeat to restrict result groups. Omitting it queries all five types.
          style: form
          explode: true
          schema:
            type: array
            uniqueItems: true
            items:
              $ref: "#/components/schemas/ReferenceOptionType"
        - name: limit
          in: query
          required: false
          description: Maximum candidates returned for each requested type. Defaults to 10.
          schema:
            type: integer
            minimum: 1
            maximum: 20
            default: 10
        - name: surface
          in: query
          required: false
          description: Comment (default) requires Agent Task-comment capability. Discuss includes ready installed Agents and active custom Agents without that capability; entitlement and Project or binding visibility still apply. Selection never grants execution authority.
          schema:
            type: string
            enum:
              - comment
              - discuss
            default: comment
      responses:
        "200":
          description: Grouped, bounded candidates that the caller may reference.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ReferenceOptionList"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/dev/workspace:
    get:
      tags:
        - Developer clients
      operationId: getCurrentDeveloperWorkspace
      summary: Read the credential-bound current Workspace
      description: Returns the current Workspace and its opaque identifier to the registered developer client. Requires `assign:read` or the CLI-only `assign:discuss` scope.
      security:
        - developerAccessToken: []
      responses:
        "200":
          description: Current Workspace
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DeveloperWorkspace"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
    put:
      tags:
        - Developer clients
      operationId: switchCurrentDeveloperWorkspace
      summary: Rotate an interactive developer credential to another Workspace
      description: |
        Validates the caller's active membership, revokes the old Workspace-bound
        developer OAuth grant and refresh family, and returns a new grant bound to the
        requested Workspace. Personal API tokens cannot switch Workspaces.
      security:
        - developerAccessToken: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/DeveloperWorkspaceSwitchRequest"
      responses:
        "200":
          description: Rotated credential and current Workspace
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DeveloperWorkspaceSwitchResult"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/dev/workspaces:
    get:
      tags:
        - Developer clients
      operationId: listDeveloperWorkspaces
      summary: List Workspaces available to an interactive developer credential
      description: Personal API tokens remain Workspace-bound and cannot enumerate other memberships.
      security:
        - developerAccessToken: []
      parameters:
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/PageLimit"
      responses:
        "200":
          description: Bounded Workspace page
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DeveloperWorkspaceList"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/cli/projects:
    get:
      tags:
        - CLI
      operationId: listCliProjects
      summary: List Projects in the credential-bound Workspace
      security:
        - bearerToken: []
        - developerAccessToken: []
      parameters:
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/PageLimit"
      responses:
        "200":
          description: Bounded Project page
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CliProjectList"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/cli/projects/{project_code}:
    get:
      tags:
        - CLI
      operationId: getCliProject
      summary: Read a Project by immutable key
      security:
        - bearerToken: []
        - developerAccessToken: []
      parameters:
        - $ref: "#/components/parameters/ProjectCode"
      responses:
        "200":
          description: CLI Project
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CliProject"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/cli/projects/{project_code}/tasks:
    get:
      tags:
        - CLI
      operationId: listCliProjectTasks
      summary: List Tasks in a Project resolved by immutable key
      security:
        - bearerToken: []
        - developerAccessToken: []
      parameters:
        - $ref: "#/components/parameters/ProjectCode"
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/PageLimit"
      responses:
        "200":
          description: Bounded CLI Task page
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CliTaskList"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
    post:
      tags:
        - CLI
      operationId: createCliProjectTask
      summary: Create a Task in a Project resolved by immutable key
      description: |
        Creates a Task through the canonical Task workflow using the Project key
        and Workspace defaults. Optional description Markdown is converted only
        through the supported safe Markdown profile. Requires `assign:write` and
        an idempotency key; Task identity remains code-addressed.
      security:
        - bearerToken: []
        - developerAccessToken: []
      parameters:
        - $ref: "#/components/parameters/ProjectCode"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CliTaskCreateRequest"
      responses:
        "201":
          description: Created CLI Task.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CliTask"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/cli/documents:
    get:
      tags:
        - CLI
      operationId: listCliDocuments
      summary: List Documents in the credential-bound Workspace
      description: Returns top-level, non-archived Document metadata, optionally limited to one Project key.
      security:
        - bearerToken: []
        - developerAccessToken: []
      parameters:
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/PageLimit"
        - name: project_code
          in: query
          required: false
          schema:
            $ref: "#/components/schemas/ProjectKey"
      responses:
        "200":
          description: Bounded Document page
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CliDocumentList"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/cli/documents/{document_path}:
    get:
      tags:
        - CLI
      operationId: getCliDocument
      summary: Read a Document by its canonical path
      description: Returns metadata and at most 65,536 characters of server-extracted text; `truncated` reports whether more text exists.
      security:
        - bearerToken: []
        - developerAccessToken: []
      parameters:
        - name: document_path
          in: path
          required: true
          schema:
            type: string
            minLength: 1
            maxLength: 200
      responses:
        "200":
          description: Bounded CLI Document
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CliDocument"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/cli/my-work:
    get:
      tags:
        - CLI
      operationId: getCliMyWork
      summary: Read the caller's bounded CLI My Work projection
      description: |
        Returns the caller's active assigned Tasks (not done) in the bearer
        token's Workspace. Rows are ordered and paginated by the server; no
        UUID is returned. A token is audience-bound to this API host and must
        carry `assign:read`.
      security:
        - bearerToken: []
        - developerAccessToken: []
      parameters:
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/PageLimit"
      responses:
        "200":
          description: A bounded My Work page
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CliTaskList"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/cli/search:
    get:
      tags:
        - CLI
      operationId: searchCliWorkspace
      summary: Search Tasks and Documents in the token-scoped Workspace
      description: |
        Searches only Task and Document projections visible to the bearer
        principal. `q` is trimmed before validation and must contain 1–200
        Unicode characters. Results use an opaque server cursor, default to
        50, and never exceed 100. Resource codes are the only identifiers
        returned; IDs remain internal to the API implementation.
      security:
        - bearerToken: []
        - developerAccessToken: []
      parameters:
        - name: q
          in: query
          required: true
          schema:
            $ref: "#/components/schemas/CliSearchQuery"
        - name: project_code
          in: query
          required: false
          schema:
            $ref: "#/components/schemas/ResourceCode"
        - name: resource_type
          in: query
          required: false
          schema:
            $ref: "#/components/schemas/CliSearchResourceType"
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/PageLimit"
      responses:
        "200":
          description: A bounded, authorized search page.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CliSearchResultList"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/cli/mcp/token:
    post:
      tags:
        - CLI
      operationId: createCliMcpBridgeToken
      summary: Create a short-lived MCP token for the local CLI bridge
      description: |
        Exchanges the caller's CLI OAuth or personal API-token authority for a
        non-refreshable MCP access token. The requested Workspace must match
        the authenticated principal, and requested scopes may only narrow the
        parent credential. The grant records its parent credential so parent
        expiry or revocation immediately invalidates the derived token.
      security:
        - bearerToken: []
        - developerAccessToken: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CliMcpBridgeTokenRequest"
      responses:
        "201":
          description: A bounded, Workspace-pinned MCP token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CliMcpBridgeTokenResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "409":
          $ref: "#/components/responses/Conflict"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/cli/milestones/{milestone_code}:
    get:
      tags:
        - CLI
      operationId: getCliMilestone
      summary: Read a Milestone by code within the bearer Workspace
      description: Resolves the code's Project key by indexed authorized lookup in the token-scoped Workspace, then uses the shared typed Milestone resolver. Includes archived Milestones and canonical Task-derived progress. ASCII casing normalizes; no generic resource dispatch, collection scan or domain write. Existing UUID and browser/native operations remain unchanged.
      security:
        - bearerToken: []
        - developerAccessToken: []
      parameters:
        - name: milestone_code
          in: path
          required: true
          schema:
            type: string
            pattern: ^[A-Za-z][A-Za-z0-9]{1,7}-[Mm][1-9][0-9]{0,18}$
            maxLength: 29
            example: FRS-M1
          description: Positive signed-64-bit Project Milestone code; numeric maximum 9223372036854775807, without whitespace, signs or leading zeros.
      responses:
        "200":
          description: Authorized Milestone and canonical progress.
          headers:
            ETag:
              $ref: "#/components/headers/ETag"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Milestone"
        "400":
          $ref: "#/components/responses/BadRequest"
          description: invalid_milestone_code for malformed syntax or numeric overflow.
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/cli/tasks/{task_code}:
    get:
      tags:
        - CLI
      operationId: getCliTask
      summary: Read a Task by code
      description: |
        Resolves the uppercase-normalized Task code within the token-scoped
        Workspace. The `revision` field and the `ETag` header carry the
        current revision that the revision-checked lifecycle operations
        expect in `If-Match`. A Task the bearer cannot read is
        indistinguishable from a missing one.
      security:
        - bearerToken: []
        - developerAccessToken: []
      parameters:
        - $ref: "#/components/parameters/TaskCode"
      responses:
        "200":
          description: CLI Task
          headers:
            ETag:
              $ref: "#/components/headers/ETag"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CliTask"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/cli/tasks/{task_code}/start:
    post:
      tags:
        - CLI
      operationId: startCliTask
      summary: Start and assign a Task to the bearer Actor
      description: |
        Resolves the uppercase-normalized Task code within the token-scoped
        Workspace, transitions it through the canonical workflow to the
        Project's active Status, and assigns it to the bearer Actor in one
        idempotent, revision-checked, audited transaction. An existing
        assignment is closed rather than erased. `reassigned` tells the CLI
        whether to warn; no prior Actor identifier is disclosed for that use.
      security:
        - bearerToken: []
        - developerAccessToken: []
      parameters:
        - $ref: "#/components/parameters/TaskCode"
        - $ref: "#/components/parameters/IdempotencyKey"
        - $ref: "#/components/parameters/IfMatch"
      responses:
        "200":
          description: Started Task and reassignment outcome
          headers:
            ETag:
              $ref: "#/components/headers/ETag"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CliTaskMutationResult"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/cli/tasks/{task_code}/done:
    post:
      tags:
        - CLI
      operationId: completeCliTask
      summary: Apply the canonical completion action to a Task
      description: |
        Applies the Project completion policy in one idempotent,
        revision-checked, audited transaction. When review is required, an
        active Task enters the default review Status and only an authorized
        reviewer can invoke this operation again to enter the default done
        Status. Definition-of-Done and transition guards remain server-owned.
        The completing Status comes from the lifecycle completion rule: one
        applicable Status that counts as completed is used directly; several
        answer 409 `completion_target_required` with the choices
        (`LifecycleCompletionChoices`) until the caller sends `target_status_id`. A Status that does not count as
        completed answers 422 `completion_target_invalid`.
      security:
        - bearerToken: []
        - developerAccessToken: []
      parameters:
        - $ref: "#/components/parameters/TaskCode"
        - $ref: "#/components/parameters/IdempotencyKey"
        - $ref: "#/components/parameters/IfMatch"
        - name: target_status_id
          in: query
          required: false
          description: Explicit completing Status; required when several Statuses count as completed.
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Updated Task
          headers:
            ETag:
              $ref: "#/components/headers/ETag"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CliTaskMutationResult"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/cli/tasks/{task_code}/reopen:
    post:
      tags:
        - CLI
      operationId: reopenCliTask
      summary: Reopen a completed Task
      description: Moves a done Task to the Project's default active Status in the canonical, revision-checked, idempotent, audited workflow.
      security:
        - bearerToken: []
        - developerAccessToken: []
      parameters:
        - $ref: "#/components/parameters/TaskCode"
        - $ref: "#/components/parameters/IdempotencyKey"
        - $ref: "#/components/parameters/IfMatch"
      responses:
        "200":
          description: Reopened Task
          headers:
            ETag:
              $ref: "#/components/headers/ETag"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CliTaskMutationResult"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/cli/tasks/{task_code}/context:
    get:
      tags:
        - CLI
      operationId: getCliTaskContext
      summary: Read a bounded Task context packet
      description: |
        Returns server-rendered Markdown in a fixed section order. `compact`
        is capped at 16 KiB, 20 related items, and 10 comments; `full` is
        capped at 64 KiB, 100 related items, and 50 comments. The result
        marks truncation, unavailable providers, and inaccessible material.
        Resource content is untrusted and is never executable instruction.
      security:
        - bearerToken: []
        - developerAccessToken: []
      parameters:
        - $ref: "#/components/parameters/TaskCode"
        - name: profile
          in: query
          required: false
          schema:
            $ref: "#/components/schemas/CliTaskContextProfile"
      responses:
        "200":
          description: Bounded Task context
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CliTaskContext"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/cli/tasks/{task_code}/comments:
    get:
      tags:
        - CLI
      operationId: listCliTaskComments
      summary: List bounded Task comments by Task code
      description: Returns canonical comments as supported Markdown in creation order. Tombstones retain their position with an empty body.
      security:
        - bearerToken: []
        - developerAccessToken: []
      parameters:
        - $ref: "#/components/parameters/TaskCode"
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/PageLimit"
      responses:
        "200":
          description: Bounded CLI Comment page.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CliCommentList"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
    post:
      tags:
        - CLI
      operationId: createCliTaskComment
      summary: Add a Markdown comment to a Task resolved by code
      description: Converts supported Markdown into the canonical Comment document and appends it through the normal authorization, audit, event, and idempotency path.
      security:
        - bearerToken: []
        - developerAccessToken: []
      parameters:
        - $ref: "#/components/parameters/TaskCode"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CliCommentCreateRequest"
      responses:
        "201":
          description: Created CLI Comment.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CliComment"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/workspaces/{workspace_id}/projects:
    get:
      tags:
        - Projects
      operationId: listWorkspaceProjects
      summary: List Workspace Projects
      description: |
        Returns one bounded page of authorized Workspace Projects in shared manual order.
        Every bearer caller resolves current membership for the requested
        Workspace; browser callers retain their current session behavior.
        A continuation binds the prior page's Workspace Project and authority
        revision. A reorder or permission change returns 409 catalog_changed
        with no items; restart the traversal from the first page.
      security:
        - browserSession: []
        - nativeAccessToken: []
        - bearerToken: []
        - developerAccessToken: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/PageLimit"
      responses:
        "200":
          description: A page of Projects.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProjectList"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
    post:
      tags:
        - Projects
      operationId: createProject
      summary: Create a Project
      description: |
        Creates a Project with its default `Backlog`/`Todo`/`In Progress`/
        `Done` workflow. `key` is the uppercase-normalized ticket prefix
        (`[A-Z][A-Z0-9]{1,7}`, unique per Workspace, immutable after
        creation); it is not a substitute for the opaque `id`.
        Accepts native mobile bearer credentials with live Workspace and Project
        authorization. Browser sessions still require CSRF; native callers omit it.
      security:
        - browserSession: []
        - nativeAccessToken: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - $ref: "#/components/parameters/BrowserCSRFToken"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ProjectCreateRequest"
      responses:
        "201":
          description: The created Project.
          headers:
            ETag:
              $ref: "#/components/headers/ETag"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Project"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/projects/{project_id}:
    get:
      tags:
        - Projects
      operationId: getProject
      summary: Read a Project
      security:
        - browserSession: []
        - nativeAccessToken: []
        - bearerToken: []
        - developerAccessToken: []
      parameters:
        - $ref: "#/components/parameters/ProjectId"
      responses:
        "200":
          description: The Project.
          headers:
            ETag:
              $ref: "#/components/headers/ETag"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Project"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
    patch:
      tags:
        - Projects
      operationId: renameProject
      summary: Update Project metadata, planning dates, path, visual identity, or visibility
      description: |
        Applies an optimistic compare-and-swap to exactly one of `name`, `path`,
        `description`, `url`, `visual_identity`, `visibility`, or `owner_actor_id`, or sends the complete nullable `start_on`/`target_on` date pair. `If-Match` must carry the revision last observed by
        the client. Send `visual_identity: null` to restore the default marker.
        Setting visibility to public requires the Workspace's public-publishing billing
        entitlement; denial returns 403 paid_workspace_required. Unpublishing remains
        available subject to normal authorization. Public reads return the existing privacy-safe
        404 while that entitlement is absent, without changing stored visibility or content.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/ProjectId"
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/IdempotencyKey"
        - $ref: "#/components/parameters/IfMatch"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ProjectRenameRequest"
      responses:
        "200":
          description: The updated Project.
          headers:
            ETag:
              $ref: "#/components/headers/ETag"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Project"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
    delete:
      tags:
        - Projects
      operationId: archiveProject
      summary: Archive a Project
      description: |
        Archives a Project with optimistic concurrency. The current Project
        revision must be supplied in `If-Match`; retries with the same
        idempotency key replay the original response.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/ProjectId"
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/IfMatch"
        - $ref: "#/components/parameters/IdempotencyKey"
      responses:
        "204":
          description: The Project is archived.
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/projects/{project_id}/overview:
    get:
      tags:
        - Projects
      operationId: getProjectOverview
      summary: Read the bounded Project overview projection
      description: |
        Returns one permission-filtered Project overview. Active Task counts,
        Status distribution, ongoing work, recent resources, milestone progress,
        member summaries, and freshness metadata are server-computed. Ongoing
        work is selected only through the immutable `in_progress` and
        `in_review` Status categories; clients must not infer it from labels or
        rebuild this response by fanning out over collection endpoints.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/ProjectId"
      responses:
        "200":
          description: The current bounded Project overview.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProjectOverview"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/projects/{project_id}/move:
    post:
      tags:
        - Projects
      operationId: moveProject
      summary: Move a Project in the Workspace's shared manual order
      description: |
        Places the Project between its optional neighbour anchors. Clients never
        send or receive a mutable position or rank. `expected_revision` is the
        Project revision being moved; a stale revision returns `409
        revision_conflict`. A missing, archived, or out-of-Workspace anchor
        returns `409 anchor_not_found`.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/ProjectId"
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ProjectMoveRequest"
      responses:
        "200":
          description: The moved Project.
          headers:
            ETag:
              $ref: "#/components/headers/ETag"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Project"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          description: A stale revision or unavailable anchor, with the current Project for re-anchoring.
          headers:
            ETag:
              $ref: "#/components/headers/ETag"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProjectMoveConflict"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/projects/{project_id}/completion-policy:
    get:
      tags:
        - Projects
      operationId: getProjectCompletionPolicy
      summary: Read a Project's completion policy
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/ProjectId"
      responses:
        "200":
          description: The Project completion policy
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProjectCompletionPolicy"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
    put:
      tags:
        - Projects
      operationId: updateProjectCompletionPolicy
      summary: Configure whether completion requires review
      description: |
        An authorized Project manager may enable review-before-completion only
        while naming a Project-applicable default review Status and default
        done Status. Disabling it retains the configured defaults but makes
        completion direct. The backend validates Status categories and all
        authorization; clients cannot bypass the policy through Task patch.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/ProjectId"
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/IdempotencyKey"
        - $ref: "#/components/parameters/IfMatch"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ProjectCompletionPolicy"
      responses:
        "200":
          description: Updated Project completion policy
          headers:
            ETag:
              $ref: "#/components/headers/ETag"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProjectCompletionPolicy"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/projects/{project_id}/statuses:
    get:
      tags:
        - Projects
      operationId: listProjectStatuses
      summary: List Project Statuses
      description: |
        Returns one workflow-ordered page combining the Project's own
        Statuses and the Workspace-wide Statuses applicable to it.
      security:
        - browserSession: []
        - nativeAccessToken: []
        - bearerToken: []
        - developerAccessToken: []
      parameters:
        - $ref: "#/components/parameters/ProjectId"
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/PageLimit"
      responses:
        "200":
          description: A page of Statuses.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/StatusList"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
    post:
      tags:
        - Projects
      operationId: createProjectStatus
      summary: Create a Project workflow Status
      description: |
        Appends a Status to the Project-scoped workflow. The caller must have
        Project management access.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/ProjectId"
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ProjectStatusCreateRequest"
      responses:
        "201":
          description: The created Project workflow Status.
          headers:
            ETag:
              $ref: "#/components/headers/ETag"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Status"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/projects/{project_id}/members:
    get:
      tags:
        - Projects
      operationId: listProjectMembers
      summary: List the Project's explicit members
      description: Returns an owner-first, stable, cursor-bounded projection of active Project membership. It never substitutes the Workspace roster and is unavailable from public Project views.
      security:
        - browserSession: []
        - nativeAccessToken: []
      parameters:
        - $ref: "#/components/parameters/ProjectId"
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/PageLimit"
      responses:
        "200":
          description: A page of Project members.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProjectMemberList"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"
    post:
      tags:
        - Projects
      operationId: addProjectMember
      summary: Add an active Workspace member to the Project
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/ProjectId"
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ProjectMemberCreateRequest"
      responses:
        "201":
          description: The Project member.
          headers:
            ETag:
              $ref: "#/components/headers/ETag"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProjectMember"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"
        "500":
          $ref: "#/components/responses/InternalServerError"
  /api/v1/projects/{project_id}/members/{actor_id}:
    patch:
      tags:
        - Projects
      operationId: updateProjectMember
      summary: Change a Project member's role
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/ProjectId"
        - name: actor_id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/Identifier"
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/IdempotencyKey"
        - $ref: "#/components/parameters/IfMatch"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ProjectMemberUpdateRequest"
      responses:
        "200":
          description: The updated Project member.
          headers:
            ETag:
              $ref: "#/components/headers/ETag"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProjectMember"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"
        "500":
          $ref: "#/components/responses/InternalServerError"
    delete:
      tags:
        - Projects
      operationId: removeProjectMember
      summary: Remove a Project member
      description: The current owner cannot be removed. Owner transfer must complete first.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/ProjectId"
        - name: actor_id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/Identifier"
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/IdempotencyKey"
        - $ref: "#/components/parameters/IfMatch"
      responses:
        "204":
          description: The Project member was removed.
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "500":
          $ref: "#/components/responses/InternalServerError"
  /api/v1/statuses/{status_id}:
    get:
      tags:
        - Projects
      operationId: getStatus
      summary: Read a workflow Status
      description: Returns one Status, including an archived Status needed to render historical Tasks.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/StatusId"
      responses:
        "200":
          description: The Status.
          headers:
            ETag:
              $ref: "#/components/headers/ETag"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Status"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
    patch:
      tags:
        - Projects
      operationId: updateStatus
      summary: Update or restore a workflow Status
      description: "Updates supplied fields. Set `archived: false` to restore an archived Status."
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/StatusId"
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/IfMatch"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/StatusUpdateRequest"
      responses:
        "200":
          description: The updated Status.
          headers:
            ETag:
              $ref: "#/components/headers/ETag"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Status"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
    delete:
      tags:
        - Projects
      operationId: archiveStatus
      summary: Archive a workflow Status
      description: |
        Archives a Status without repointing Tasks. It returns `409 status_in_use`
        with the live Task count when live Tasks still reference it, and
        `409 required_status` when it is the last active `todo`, `in_progress`,
        or `done` Status in its own scope.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/StatusId"
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/IfMatch"
        - $ref: "#/components/parameters/IdempotencyKey"
      responses:
        "204":
          description: The Status is archived.
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/statuses/{status_id}/move:
    post:
      tags:
        - Projects
      operationId: moveStatus
      summary: Move a Status in its workflow order
      description: |
        Moves a Status between optional neighbour anchors within its own
        Workspace-wide or Project-scoped workflow. Cross-scope or stale anchors
        return `409 anchor_not_found`; the server owns integer positions.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/StatusId"
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/StatusMoveRequest"
      responses:
        "200":
          description: The moved Status.
          headers:
            ETag:
              $ref: "#/components/headers/ETag"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Status"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/statuses/{status_id}/lifecycle-preview:
    get:
      tags:
        - Projects
      operationId: previewStatusLifecycleChange
      summary: Preview a Status lifecycle migration
      description: Returns the authoritative number of Tasks whose Active/Resolved lifecycle fields would change. Synchronous migration is refused when the count exceeds the returned limit.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/StatusId"
        - name: status_type
          in: query
          required: true
          schema:
            $ref: "#/components/schemas/StatusType"
      responses:
        "200":
          description: The bounded lifecycle migration preview.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/StatusLifecyclePreview"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"
  /api/v1/statuses/{status_id}/archive-and-replace:
    post:
      tags:
        - Projects
      operationId: archiveStatusAndReplace
      summary: Archive a workflow Status and replace its live Tasks
      description: |
        Atomically moves every non-archived Task that references this Status to
        an active replacement Status in the exact same Workspace-wide or
        Project-scoped workflow, then archives the source. The source revision
        is checked through `If-Match`; an archived, cross-scope, missing, or
        self-replacement returns `409 replacement_status_unavailable`.

        The operation is refused with `409 required_status` when archiving the
        source would remove the final active `todo`, `in_progress`, or `done`
        Status in its scope. Historical archived Tasks keep their source
        Status unchanged.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/StatusId"
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/IfMatch"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/StatusArchiveAndReplaceRequest"
      responses:
        "200":
          description: The archived source Status after live Tasks move to its replacement.
          headers:
            ETag:
              $ref: "#/components/headers/ETag"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Status"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/workspaces/{workspace_id}/tasks:
    get:
      tags:
        - Tasks
      operationId: listWorkspaceTasks
      summary: List bounded Workspace Tasks
      description: |
        Returns a newest-updated-first cursor page of Tasks in the current
        Workspace. It is an activity collection, not a text-search endpoint.
        Repeated `project_id` and `status_category` filters use OR semantics;
        a Project outside the Workspace simply contributes no rows.
        With `consistency=checked`, Core binds continuation to the normalized filters,
        caller, collection version and visibility revision. Complete coverage requires
        an unchanged scan ending without a cursor. Changed data or permissions returns
        an empty stale page without continuation. Ten pages or 1,000 rows caps a scan;
        an incomplete capped result remains partial. Restart stale scans explicitly.
        Omitted consistency preserves legacy pagination without a completeness claim.
      security:
        - browserSession: []
        - nativeAccessToken: []
        - bearerToken: []
        - developerAccessToken: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/PageLimit"
        - name: project_id
          in: query
          description: Restrict to one or more Project identifiers.
          schema:
            type: array
            maxItems: 100
            items:
              $ref: "#/components/schemas/Identifier"
          style: form
          explode: true
        - name: status_category
          in: query
          description: Restrict to one or more Status lifecycle categories.
          schema:
            type: array
            maxItems: 5
            items:
              $ref: "#/components/schemas/StatusCategory"
          style: form
          explode: true
        - name: behavior
          in: query
          description: |
            Keep Tasks whose current stage has every named lifecycle behavior
            enabled (AND semantics, at most 8 distinct keys). Keys are the
            Task-applicable entries of `listLifecycleBehaviors`; an unknown or
            non-Task key is a 400. Totals and cursors honor the filter.
          schema:
            type: array
            maxItems: 8
            items:
              $ref: "#/components/schemas/LifecycleBehaviorKey"
          style: form
          explode: true
        - name: assignee_actor_id
          in: query
          schema:
            $ref: "#/components/schemas/Identifier"
        - name: milestone_id
          in: query
          schema:
            $ref: "#/components/schemas/Identifier"
        - name: updated_since
          in: query
          description: Include Tasks updated at or after this instant.
          schema:
            $ref: "#/components/schemas/Timestamp"
        - name: include_archived
          in: query
          description: Include archived Tasks; omitted or false returns active Tasks only.
          schema:
            type: boolean
            default: false
        - name: consistency
          in: query
          description: Opt into service-derived coverage and version-bound pagination.
          schema:
            type: string
            enum:
              - checked
        - name: state
          in: query
          description: Active means unresolved; omitted or all includes active and resolved Tasks.
          schema:
            type: string
            enum:
              - active
              - resolved
              - all
            default: all
        - name: resolution
          in: query
          description: Restrict to completed or cancelled resolution. Incompatible with active state.
          schema:
            type: string
            enum:
              - completed
              - cancelled
      responses:
        "200":
          description: A bounded Workspace Task page.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TaskList"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/workspaces/{workspace_id}/tasks/{task_code}:
    get:
      tags:
        - Tasks
      operationId: getTaskByCode
      summary: Resolve a Task's canonical Workspace URL
      description: |
        Resolves the stable, case-insensitive Task code inside the current
        Workspace after Workspace and Project read authorization. Unlike active
        Task collections, this route returns archived and trashed Tasks during
        recovery and the safe Task tombstone after purge so canonical browser
        URLs remain durable without exposing content removed by retention.
        Native bearer reads use live Workspace membership and the same resource
        visibility. Invalid native credentials never fall back to browser cookies.
      security:
        - browserSession: []
        - nativeAccessToken: []
        - bearerToken: []
        - developerAccessToken: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - $ref: "#/components/parameters/TaskCode"
      responses:
        "200":
          description: The authorized Task lifecycle representation.
          headers:
            ETag:
              $ref: "#/components/headers/ETag"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Task"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/workspaces/{workspace_id}/tasks/bulk:
    post:
      tags:
        - Tasks
      operationId: bulkUpdateTasks
      summary: Update up to 100 explicitly selected Tasks
      description: |
        Applies the same patch independently to one through 100 explicit Task
        IDs in the current Workspace. Every item carries the revision it was
        selected from; authorization and compare-and-swap checks happen per
        Task, so selection never grants access. The response always contains
        one outcome per requested item in request order. Retry only failed
        items with their then-current revisions and a fresh idempotency key.

        This initial synchronous command supports status, assignee, priority,
        due date, and milestone changes. It has no snapshot-query input,
        asynchronous job, progress, or cancellation contract; requests over
        100 items are rejected. Archive/restore and labels remain separate
        operations with their own accepted lifecycle rules.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/BulkTaskUpdateRequest"
      responses:
        "200":
          description: Per-Task bulk-update results, including partial failures.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BulkTaskUpdateResult"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "413":
          $ref: "#/components/responses/PayloadTooLarge"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/workspaces/{workspace_id}/work:
    get:
      tags:
        - Tasks
      operationId: listMyWork
      summary: Read the caller's bounded My Work view
      description: |
        Returns one caller-scoped Task view. The server obtains the actor from
        the authenticated principal; no actor filter is accepted. Browser
        sessions and bearer credentials resolve the same live caller
        authorization; bearer credentials never inherit browser cookies.
        Date membership uses the actor's persisted IANA timezone and the
        returned `as_of_date`.
      security:
        - browserSession: []
        - nativeAccessToken: []
        - bearerToken: []
        - developerAccessToken: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/PageLimit"
        - name: view
          in: query
          required: true
          schema:
            $ref: "#/components/schemas/MyWorkView"
      responses:
        "200":
          description: A bounded caller Work page
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MyWorkPage"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/workspaces/{workspace_id}/subscribed-tasks:
    get:
      tags:
        - Tasks
      operationId: listSubscribedTasks
      summary: Read the caller's actively followed Tasks
      description: |
        Returns Tasks the caller currently follows (subscription state
        `following`) in the Workspace, newest subscription activity first.
        The server obtains the actor from the authenticated principal; no
        actor filter is accepted. Muted and unfollowed subscriptions are
        excluded, as are Tasks in a private Project the caller cannot see.
      security:
        - browserSession: []
        - nativeAccessToken: []
        - bearerToken: []
        - developerAccessToken: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/PageLimit"
      responses:
        "200":
          description: A bounded page of followed Tasks
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SubscribedTaskPage"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/projects/{project_id}/tasks:
    get:
      tags:
        - Tasks
      operationId: listProjectTasks
      summary: List Project Tasks
      description: |
        Returns one page ordered by the Task's immutable project-local
        number by default. Column orders require exactly one status_id, exclude
        archived Tasks, and return a snapshot-consistent column total and advisory
        movement permission instead of global history counts. Cursors bind the
        Project, Status, order and filters. Rank pagination is a live traversal;
        concurrent moves can require refreshing earlier pages.
      security:
        - browserSession: []
        - nativeAccessToken: []
        - bearerToken: []
        - developerAccessToken: []
      parameters:
        - $ref: "#/components/parameters/ProjectId"
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/PageLimit"
        - name: state
          in: query
          schema:
            type: string
            enum:
              - active
              - resolved
              - all
            default: all
          description: Filter by the lifecycle umbrella derived from current resolution facts.
        - name: status_id
          in: query
          schema:
            type: array
            maxItems: 100
            items:
              $ref: "#/components/schemas/Identifier"
          style: form
          explode: true
        - name: assignee_actor_id
          in: query
          schema:
            $ref: "#/components/schemas/Identifier"
        - name: resolution
          in: query
          schema:
            $ref: "#/components/schemas/TaskResolution"
        - name: include_archived
          in: query
          schema:
            type: boolean
            default: false
        - name: order
          in: query
          schema:
            type: string
            enum:
              - number
              - rank
              - rank_desc
              - resolved_desc
            default: number
          description: Column rank order or newest resolution first; number retains history semantics.
        - name: resolved_since
          in: query
          schema:
            type: string
            format: date-time
          description: Inclusive recent-resolution cutoff for column orders; does not restrict column.total.
        - name: anchor_task_id
          in: query
          schema:
            $ref: "#/components/schemas/Identifier"
          description: Start strictly after this current Task in the chosen rank direction. Must belong to this Project and Status. Cannot accompany cursor or resolved_desc. Other history filters are not accepted with column orders.
        - name: milestone_id
          in: query
          description: Match any selected milestone before pagination. Supported with number order only; an unknown or other-Project ID matches no Tasks.
          schema:
            type: array
            maxItems: 100
            items:
              $ref: "#/components/schemas/Identifier"
          style: form
          explode: true
      responses:
        "200":
          description: A page of Tasks.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProjectTaskList"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
    post:
      tags:
        - Tasks
      operationId: createTask
      summary: Create a Task
      description: |
        Canonical content with an unsupported schema or an attempted
        schema 2-to-1 downgrade returns 409 document_schema_version_unsupported.
        Restoring an older Document revision retains an existing schema 2 floor.
        Creates a Task with an atomically allocated project-local number.
        Accepts scoped bearer credentials with live Workspace and Project
        authorization. Browser sessions still require CSRF; bearer callers omit it.
      security:
        - browserSession: []
        - nativeAccessToken: []
        - bearerToken: []
        - developerAccessToken: []
      parameters:
        - $ref: "#/components/parameters/ProjectId"
        - $ref: "#/components/parameters/BrowserCSRFToken"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TaskCreateRequest"
      responses:
        "201":
          description: The created Task.
          headers:
            ETag:
              $ref: "#/components/headers/ETag"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Task"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/DocumentSchemaUnsupported"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/projects/{project_id}/milestones:
    get:
      tags:
        - Projects
      operationId: listProjectMilestones
      summary: List Project milestones
      description: Every returned milestone includes canonical progress for its non-archived, non-trashed Tasks, aggregated independently of loaded Task pages.
      security:
        - browserSession: []
        - nativeAccessToken: []
      parameters:
        - $ref: "#/components/parameters/ProjectId"
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/PageLimit"
      responses:
        "200":
          description: A page of milestones.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MilestoneList"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
    post:
      tags:
        - Projects
      operationId: createMilestone
      summary: Create a Project milestone
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/ProjectId"
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/MilestoneCreateRequest"
      responses:
        "201":
          description: The created milestone.
          headers:
            ETag:
              $ref: "#/components/headers/ETag"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Milestone"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
          description: Idempotency conflict or milestone_reference_exhausted when the Project has issued every positive signed-64-bit reference number. No Milestone or event is committed on exhaustion.
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/projects/{project_id}/milestones/by-code/{milestone_code}:
    get:
      tags:
        - Projects
      operationId: getMilestoneByCode
      summary: Resolve a Milestone code within an authorized Project
      description: Resolves only Milestones in the selected Workspace and Project, including archived Milestones. ASCII input casing is normalized; canonical output is uppercase. Codes do not grant access or fall through to Tasks or Documents. Invalid syntax or signed-64-bit overflow is rejected. Existing UUID operations remain valid.
      security:
        - browserSession: []
        - nativeAccessToken: []
      parameters:
        - $ref: "#/components/parameters/ProjectId"
        - name: milestone_code
          in: path
          required: true
          schema:
            type: string
            pattern: ^[A-Za-z][A-Za-z0-9]{1,7}-[Mm][1-9][0-9]{0,18}$
            maxLength: 29
            example: FRS-M1
          description: Project-key Milestone code with a positive signed-64-bit number, at most 9223372036854775807. No leading zeros or whitespace.
      responses:
        "200":
          description: The authorized Milestone and canonical Task-derived progress.
          headers:
            ETag:
              $ref: "#/components/headers/ETag"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Milestone"
        "400":
          $ref: "#/components/responses/BadRequest"
          description: invalid_milestone_code for malformed syntax or numeric overflow.
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/milestones/{milestone_id}:
    get:
      tags:
        - Projects
      operationId: getMilestone
      summary: Read a milestone with computed progress
      security:
        - browserSession: []
        - nativeAccessToken: []
      parameters:
        - $ref: "#/components/parameters/MilestoneId"
      responses:
        "200":
          description: The milestone.
          headers:
            ETag:
              $ref: "#/components/headers/ETag"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Milestone"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
    patch:
      tags:
        - Projects
      operationId: updateMilestone
      summary: Update a milestone
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/MilestoneId"
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/IfMatch"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/MilestoneUpdateRequest"
      responses:
        "200":
          description: The updated milestone.
          headers:
            ETag:
              $ref: "#/components/headers/ETag"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Milestone"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
    delete:
      tags:
        - Projects
      operationId: archiveMilestone
      summary: Archive a milestone
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/MilestoneId"
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/IfMatch"
        - $ref: "#/components/parameters/IdempotencyKey"
      responses:
        "200":
          description: The archived milestone.
          headers:
            ETag:
              $ref: "#/components/headers/ETag"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Milestone"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/projects/{project_id}/attachments:
    get:
      tags:
        - Projects
      operationId: listProjectAttachments
      summary: List Project attachments
      description: Returns the newest 100 linked files. Optional attachment_id values select up to 100 exact linked targets regardless of age; deleted, unlinked and private Discuss files are omitted after parent authorization.
      security:
        - browserSession: []
        - nativeAccessToken: []
      parameters:
        - $ref: "#/components/parameters/ProjectId"
        - $ref: "#/components/parameters/AttachmentTargetIds"
      responses:
        "200":
          description: Project attachments
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AttachmentList"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
    post:
      tags:
        - Projects
      operationId: attachProjectAttachment
      summary: Attach a completed upload to a Project
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/ProjectId"
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AttachmentLinkRequest"
      responses:
        "201":
          description: Attached
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Attachment"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/projects/{project_id}/task-attachments:
    get:
      tags:
        - Projects
      operationId: listProjectTaskAttachments
      summary: List attachments of the Project's Tasks
      description: Newest-first files attached to the Project's current Tasks. A Task moved to another Project takes its files with it.
      security:
        - browserSession: []
        - nativeAccessToken: []
      parameters:
        - $ref: "#/components/parameters/ProjectId"
      responses:
        "200":
          description: Project Task attachments
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AttachmentList"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/tasks/{task_id}/relations:
    get:
      tags:
        - Tasks
      operationId: listTaskRelations
      summary: List Task relations
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/TaskId"
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/PageLimit"
      responses:
        "200":
          description: Task relations
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TaskRelationList"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
    post:
      tags:
        - Tasks
      operationId: createTaskRelation
      summary: Create a Task relation
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/TaskId"
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TaskRelationCreateRequest"
      responses:
        "200":
          description: Existing relation returned for an idempotent replay
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TaskRelation"
        "201":
          description: Created relation
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TaskRelation"
        "400":
          $ref: "#/components/responses/BadRequest"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/integration-catalog:
    get:
      tags:
        - Integrations
      operationId: listIntegrationCatalog
      summary: List Assign integration providers
      description: Returns the bounded, server-owned provider catalog. Availability describes product readiness; it does not imply that the current Workspace has installed or authorized a provider.
      security:
        - browserSession: []
      responses:
        "200":
          description: Integration provider catalog
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationProviderList"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/workspaces/{workspace_id}/integration-installations:
    get:
      tags:
        - Integrations
      operationId: listWorkspaceIntegrationInstallations
      summary: List current Workspace integration installations
      description: |
        Returns a display-safe, persisted-only projection of current provider
        installations for the session Workspace. The operation never calls a
        provider and excludes credentials, external tenant identifiers, grants,
        provider metadata, personal connections, and disconnected history.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceRealtimeBaseline"
        - $ref: "#/components/parameters/WorkspaceId"
        - $ref: "#/components/parameters/Cursor"
        - name: limit
          in: query
          required: false
          description: Maximum current installations to return.
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
      responses:
        "200":
          description: Current Workspace integration installations
          headers:
            X-Assign-Realtime-Cursor:
              $ref: "#/components/headers/RealtimeReadCursor"
            X-Assign-Realtime-Position:
              $ref: "#/components/headers/RealtimeReadPosition"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationInstallationList"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
          description: Maintenance or canonical baseline unavailable (realtime_baseline_unavailable). No cursor is returned; honor Retry-After when present.
  /api/v1/workspaces/{workspace_id}/integration-authorizations:
    post:
      tags:
        - Integrations
      operationId: beginIntegrationAuthorization
      summary: Begin an installation or personal provider authorization
      description: Creates a ten-minute single-use state and a provider authorization ceremony, using PKCE S256 when supported or confidential client authentication when required by the provider. Owners/Admins may create Workspace installation authority; every member may authorize only their own delegated identity inside an existing installation. Secret material and PKCE verifiers are never returned.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - $ref: "#/components/parameters/CSRFToken"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/IntegrationAuthorizationInput"
      responses:
        "201":
          description: Authorization ceremony created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationAuthorization"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/workspaces/{workspace_id}/integration-private-connectors:
    post:
      tags:
        - Integrations
      operationId: enrollIntegrationPrivateConnector
      summary: Enroll an outbound private-network integration connector
      description: Creates a Workspace-scoped connector identity and returns its bearer token exactly once. Owners/Admins must transfer the token directly into the connector process; Assign never returns it again.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - $ref: "#/components/parameters/CSRFToken"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/IntegrationPrivateConnectorEnrollmentInput"
      responses:
        "201":
          description: Connector enrollment created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationPrivateConnectorEnrollment"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/workspaces/{workspace_id}/integration-authorizations/credential:
    post:
      tags:
        - Integrations
      operationId: completeIntegrationAuthorizationWithCredential
      summary: Complete a provider authorization with a user-supplied credential
      description: Claims the matching unexpired installation state for the same signed-in Workspace actor, validates the credential directly with a provider that has no scoped OAuth flow, stores it in the approved vault, and persists only opaque references. The credential is accepted only in this authenticated JSON body and is never returned, placed in a URL, or written to browser storage. Replay and cross-actor completion are rejected.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - $ref: "#/components/parameters/CSRFToken"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/IntegrationCredentialCompletionInput"
      responses:
        "200":
          description: Completed Workspace installation
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationInstallation"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/integration-authorizations/callback:
    get:
      tags:
        - Integrations
      operationId: completeIntegrationAuthorization
      summary: Complete a provider authorization callback
      description: Claims one unexpired authorization attempt, exchanges an OAuth code with the provider's accepted PKCE or confidential-client flow or verifies a provider-native installation callback, stores credentials in the approved vault, persists only opaque references, and redirects to the bound Assign return path. Standard OAuth and GitHub App callbacks carry the one-use query state. A Sentry App post-install callback carries code, installationId, and orgSlug and is bound to the initiating browser by a short-lived, callback-scoped HttpOnly Secure SameSite=Lax cookie. Replay is rejected.
      security: []
      parameters:
        - name: state
          in: query
          required: false
          description: One-use authorization state. Required for standard OAuth and GitHub App callbacks; omitted only by a Sentry App post-install callback, which uses the callback-scoped browser cookie established by the authenticated start operation.
          schema:
            type: string
            minLength: 32
            maxLength: 256
        - name: code
          in: query
          required: false
          description: OAuth authorization code. Required for OAuth callbacks and omitted by provider-native installation callbacks.
          schema:
            type: string
            minLength: 1
            maxLength: 4096
        - name: installation_id
          in: query
          required: false
          description: Provider-native installation identifier. Accepted only for the matching installation authorization attempt.
          schema:
            type: string
            minLength: 1
            maxLength: 128
        - name: installationId
          in: query
          required: false
          description: Sentry App installation UUID supplied by Sentry's post-install redirect. Accepted only with code and orgSlug for the cookie-bound Sentry authorization attempt.
          schema:
            type: string
            minLength: 1
            maxLength: 128
        - name: orgSlug
          in: query
          required: false
          description: Sentry organization slug supplied by Sentry's post-install redirect. Accepted only with code and installationId for the cookie-bound Sentry authorization attempt.
          schema:
            type: string
            minLength: 1
            maxLength: 100
        - name: setup_action
          in: query
          required: false
          description: Provider-native installation outcome passed through to the matching adapter.
          schema:
            type: string
            minLength: 1
            maxLength: 32
      responses:
        "200":
          description: Completed installation for an explicit application/json client
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationInstallation"
        "303":
          description: Redirect to the bound Assign setup page for browser navigation.
        "400":
          $ref: "#/components/responses/BadRequest"
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/me/integration-identities:
    get:
      tags:
        - Integrations
      operationId: listPersonalIntegrationIdentities
      summary: List personal integration identities
      description: |
        Returns a bounded, persisted-only projection of the current User's
        passive provider identities and derived delegated-authorization state
        in the session Workspace. The operation never calls a provider and
        excludes stable remote user identifiers, credential references,
        granted scopes, provider payloads, other Users, and disconnected
        identity or installation history.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceRealtimeBaseline"
        - $ref: "#/components/parameters/Cursor"
        - name: limit
          in: query
          required: false
          description: Maximum personal integration identities to return.
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
      responses:
        "200":
          description: Personal integration identities in the session Workspace
          headers:
            X-Assign-Realtime-Cursor:
              $ref: "#/components/headers/RealtimeReadCursor"
            X-Assign-Realtime-Position:
              $ref: "#/components/headers/RealtimeReadPosition"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PersonalIntegrationIdentityList"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
          description: Maintenance or canonical baseline unavailable (realtime_baseline_unavailable). No cursor is returned; honor Retry-After when present.
  /api/v1/me/integration-identities/{identity_id}:
    delete:
      tags:
        - Integrations
      operationId: disconnectPersonalIntegrationIdentity
      summary: Disconnect the current User's delegated provider identity
      description: Revokes only the current User's delegated credential, preserves the Workspace installation and other Users, and records an immutable safe audit event. Local revocation is authoritative if provider revocation is unavailable.
      security:
        - browserSession: []
      parameters:
        - name: identity_id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/Identifier"
        - $ref: "#/components/parameters/IfMatch"
        - $ref: "#/components/parameters/CSRFToken"
      responses:
        "200":
          description: Disconnected personal integration identity
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PersonalIntegrationIdentity"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/workspaces/{workspace_id}/integration-requests:
    post:
      tags:
        - Integrations
      operationId: requestIntegrationProvider
      summary: Request an integration provider
      description: |
        Records explicit Marketplace product demand for the session Workspace.
        The requesting Actor is derived from the authenticated session. The
        operation performs no provider call and creates no installation,
        credential, grant, binding, or notification. Repeating a normalized
        provider name as the same Actor returns the same request identifier and
        updates the submitted display name and newly supplied use case.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - $ref: "#/components/parameters/CSRFToken"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/IntegrationProviderRequestInput"
      responses:
        "200":
          description: Provider demand recorded or its normalized prior request updated.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationProviderRequest"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          description: The requested provider is already present in the current catalog (`integration_already_available`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/integration-installations/{installation_id}:
    get:
      tags:
        - Integrations
      operationId: getIntegrationInstallation
      summary: Read one current Workspace integration installation
      description: |
        Returns the same display-safe, persisted-only projection as the current
        Workspace installation collection. The installation must belong to the
        session Workspace and must not be disconnected. The operation never
        calls a provider and excludes credentials, external tenant identifiers,
        grants, provider metadata, personal connections, and raw provider state.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceRealtimeBaseline"
        - name: installation_id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/Identifier"
      responses:
        "200":
          description: Current Workspace integration installation
          headers:
            X-Assign-Realtime-Cursor:
              $ref: "#/components/headers/RealtimeReadCursor"
            X-Assign-Realtime-Position:
              $ref: "#/components/headers/RealtimeReadPosition"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationInstallation"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
          description: Maintenance or canonical baseline unavailable (realtime_baseline_unavailable). No cursor is returned; honor Retry-After when present.
    delete:
      tags:
        - Integrations
      operationId: disconnectIntegrationInstallation
      summary: Disconnect an integration installation
      description: Owners/Admins stop new provider work, revoke local credential references, disconnect resources/bindings, disable subscriptions, and preserve historical links and safe audit. Canonical Assign work is never deleted.
      security:
        - browserSession: []
      parameters:
        - name: installation_id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/Identifier"
        - $ref: "#/components/parameters/IfMatch"
        - $ref: "#/components/parameters/CSRFToken"
      responses:
        "200":
          description: Disconnected installation
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationInstallation"
        "400":
          $ref: "#/components/responses/BadRequest"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/integration-installations/{installation_id}/resources:
    get:
      tags:
        - Integrations
      operationId: listIntegrationResources
      summary: List persisted remote resources
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceRealtimeBaseline"
        - name: installation_id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/Identifier"
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
      responses:
        "200":
          description: Current discovered resources
          headers:
            X-Assign-Realtime-Cursor:
              $ref: "#/components/headers/RealtimeReadCursor"
            X-Assign-Realtime-Position:
              $ref: "#/components/headers/RealtimeReadPosition"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationResourceList"
        "400":
          $ref: "#/components/responses/BadRequest"
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
          description: Maintenance or canonical baseline unavailable (realtime_baseline_unavailable). No cursor is returned; honor Retry-After when present.
  /api/v1/integration-installations/{installation_id}/management-link:
    post:
      tags:
        - Integrations
      operationId: getIntegrationInstallationManagement
      summary: Create a provider installation-management handoff
      description: |
        Owners/Admins request a fresh provider-owned settings URL for one
        active Workspace installation. The provider call occurs outside the
        database transaction. The response contains only the validated URL and
        a bounded, display-safe snapshot of current repository selection and
        granted permission levels; it never exposes credentials or raw provider
        metadata. Providers without a management surface return
        `provider_capability_unavailable`.
      security:
        - browserSession: []
      parameters:
        - name: installation_id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/Identifier"
        - $ref: "#/components/parameters/CSRFToken"
      responses:
        "200":
          description: Fresh provider management handoff.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationInstallationManagement"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/integration-installations/{installation_id}/resources/refresh:
    post:
      tags:
        - Integrations
      operationId: refreshIntegrationResources
      summary: Refresh one bounded page of provider resources
      description: Owners/Admins perform a bounded provider discovery call outside the database transaction; normalized stable resources are then upserted and audited.
      security:
        - browserSession: []
      parameters:
        - name: installation_id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/Identifier"
        - name: cursor
          in: query
          required: false
          schema:
            type: string
            maxLength: 4096
        - $ref: "#/components/parameters/CSRFToken"
      responses:
        "200":
          description: Refreshed resource page
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationResourceList"
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/projects/{project_id}/integration-bindings:
    get:
      tags:
        - Integrations
      operationId: listProjectIntegrationBindings
      summary: List current integration bindings for a Project
      description: |
        Returns a cursor-bounded, display-safe, persisted-only projection of
        current provider installations, remote resources, Project bindings,
        and aggregate binding-scoped subscription state. The Project must be
        readable in the session Workspace. The operation never calls a
        provider and excludes provider-stable resource and tenant identifiers,
        credential references, scopes, personal identities, raw subscription
        configuration, provider payloads, and disconnected history. A binding
        with zero subscriptions is valid and enables no ongoing behavior.
      security:
        - browserSession: []
        - nativeAccessToken: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceRealtimeBaseline"
        - $ref: "#/components/parameters/ProjectId"
        - $ref: "#/components/parameters/Cursor"
        - name: limit
          in: query
          required: false
          description: Maximum Project integration bindings to return.
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
      responses:
        "200":
          description: Current display-safe Project integration bindings
          headers:
            X-Assign-Realtime-Cursor:
              $ref: "#/components/headers/RealtimeReadCursor"
            X-Assign-Realtime-Position:
              $ref: "#/components/headers/RealtimeReadPosition"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProjectIntegrationBindingList"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
          description: Maintenance or canonical baseline unavailable (realtime_baseline_unavailable). No cursor is returned; honor Retry-After when present.
    post:
      tags:
        - Integrations
      operationId: createProjectIntegrationBinding
      summary: Bind a discovered remote resource to a Project
      description: Owners/Admins create or reactivate a binding. Connecting a GitHub repository, GitLab project or Bitbucket repository atomically enables incoming development linkage and queues reconciliation when no development subscription exists. Existing subscriptions, including paused or disabled behavior, are preserved. Task status mappings and provider writeback remain off. Other resources create no implicit ongoing behavior.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/ProjectId"
        - $ref: "#/components/parameters/CSRFToken"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/IntegrationBindingInput"
      responses:
        "201":
          description: Project binding created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationBinding"
        "400":
          $ref: "#/components/responses/BadRequest"
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/integration-bindings/{binding_id}:
    delete:
      tags:
        - Integrations
      operationId: disconnectIntegrationBinding
      summary: Disconnect a Project integration binding
      description: Owners/Admins disconnect the binding and disable every binding-owned subscription without deleting the remote resource or Project.
      security:
        - browserSession: []
      parameters:
        - name: binding_id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/Identifier"
        - $ref: "#/components/parameters/IfMatch"
        - $ref: "#/components/parameters/CSRFToken"
      responses:
        "200":
          description: Disconnected binding
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationBinding"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
  /api/v1/integration-bindings/{binding_id}/subscriptions:
    get:
      tags:
        - Integrations
      operationId: listIntegrationSubscriptions
      summary: List configured behavior for one binding
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceRealtimeBaseline"
        - name: binding_id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/Identifier"
      responses:
        "200":
          description: Binding subscriptions
          headers:
            X-Assign-Realtime-Cursor:
              $ref: "#/components/headers/RealtimeReadCursor"
            X-Assign-Realtime-Position:
              $ref: "#/components/headers/RealtimeReadPosition"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationSubscriptionList"
        "400":
          $ref: "#/components/responses/BadRequest"
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
          description: Maintenance or canonical baseline unavailable (realtime_baseline_unavailable). No cursor is returned; honor Retry-After when present.
    post:
      tags:
        - Integrations
      operationId: createIntegrationSubscription
      summary: Configure explicit ongoing integration behavior
      description: Owners/Admins configure typed event filters and an explicit direction. All writeback remains off unless the submitted direction and provider-specific capability explicitly enable it.
      security:
        - browserSession: []
      parameters:
        - name: binding_id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/Identifier"
        - $ref: "#/components/parameters/CSRFToken"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/IntegrationSubscriptionInput"
      responses:
        "201":
          description: Subscription created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationSubscription"
        "400":
          $ref: "#/components/responses/BadRequest"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
  /api/v1/integration-bindings/{binding_id}/sentry-readiness:
    post:
      tags:
        - Integrations
      operationId: verifySentryBinding
      summary: Verify a Sentry Project binding
      description: Owners/Admins verify the persisted installation, Project binding, enabled behavior, and current provider authorization without creating a Task or changing Sentry.
      security:
        - browserSession: []
      parameters:
        - name: binding_id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/Identifier"
        - $ref: "#/components/parameters/CSRFToken"
      responses:
        "200":
          description: Safe Sentry readiness result
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SentryBindingReadiness"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/integration-subscriptions/{subscription_id}:
    patch:
      tags:
        - Integrations
      operationId: updateIntegrationSubscription
      summary: Update, pause, resume, or disable integration behavior
      security:
        - browserSession: []
      parameters:
        - name: subscription_id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/Identifier"
        - $ref: "#/components/parameters/IfMatch"
        - $ref: "#/components/parameters/CSRFToken"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/IntegrationSubscriptionInput"
      responses:
        "200":
          description: Updated subscription
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationSubscription"
        "400":
          $ref: "#/components/responses/BadRequest"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
  /api/v1/integration-installations/{installation_id}/actions:
    get:
      tags:
        - Integrations
      operationId: listIntegrationActionDefinitions
      summary: List typed actions supported by an installation
      description: |
        Returns display metadata, authority and confirmation requirements,
        binding requirements, and a bounded JSON input schema. It returns no
        credentials or provider grants. GitHub installations with a current
        user Gist grant can declare `github.gist.import`,
        `github.gist.export`, and `github.gist.link`; these are one-shot
        operations and never create an implicit synchronization subscription.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceRealtimeBaseline"
        - name: installation_id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/Identifier"
      responses:
        "200":
          description: Typed remote actions
          headers:
            X-Assign-Realtime-Cursor:
              $ref: "#/components/headers/RealtimeReadCursor"
            X-Assign-Realtime-Position:
              $ref: "#/components/headers/RealtimeReadPosition"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationActionDefinitionList"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
          description: Maintenance or canonical baseline unavailable (realtime_baseline_unavailable). No cursor is returned; honor Retry-After when present.
  /api/v1/integration-actions:
    post:
      tags:
        - Integrations
      operationId: requestIntegrationAction
      summary: Request an idempotent typed remote action
      description: |
        Intersects the Actor's integration/work capability, binding, provider
        grant, declared installation/user authority, confirmation requirement,
        idempotency fingerprint, and persisted rate budget before durable
        execution. A `github.gist.export` input with `visibility: secret` is
        invalid unless `secret_visibility_ack` is exactly
        `secret_gists_are_not_private`. Gist actions always use `user`
        authority; installation authority cannot substitute.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/IntegrationActionInput"
      responses:
        "202":
          description: Action accepted or idempotently replayed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationAction"
        "400":
          $ref: "#/components/responses/BadRequest"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/integration-actions/{action_id}:
    get:
      tags:
        - Integrations
      operationId: getIntegrationAction
      summary: Read durable remote-action status
      description: Read after an accepted action, an integrations invalidation, or recovery; reading never repeats the provider action. Results are bounded normalized provider output and never contain credentials.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceRealtimeBaseline"
        - name: action_id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/Identifier"
      responses:
        "200":
          description: Current action state
          headers:
            X-Assign-Realtime-Cursor:
              $ref: "#/components/headers/RealtimeReadCursor"
            X-Assign-Realtime-Position:
              $ref: "#/components/headers/RealtimeReadPosition"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationAction"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
          description: Maintenance or canonical baseline unavailable (realtime_baseline_unavailable). No cursor is returned; honor Retry-After when present.
  /api/v1/webhooks/integrations/{provider_key}/{installation_id}:
    post:
      tags:
        - Integrations
      operationId: receiveIntegrationWebhook
      summary: Verify and durably enqueue a provider webhook
      description: The provider adapter verifies the raw bounded request before Assign persists only a normalized envelope and digest. Provider delivery identity deduplicates retries. Domain/provider fan-out never occurs before the 202 response.
      security: []
      parameters:
        - name: provider_key
          in: path
          required: true
          schema:
            type: string
            pattern: ^[a-z][a-z0-9_]*$
            minLength: 2
            maxLength: 50
        - name: installation_id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/Identifier"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              maxProperties: 256
              additionalProperties: true
      responses:
        "202":
          description: Verified delivery durably accepted
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationWebhookReceipt"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/webhooks/integrations/{provider_key}:
    post:
      tags:
        - Integrations
      operationId: receiveGlobalIntegrationWebhook
      summary: Verify and route a provider-global webhook
      description: A configured global provider adapter verifies the bounded raw request before Assign either returns a provider-required synchronous handshake or interaction acknowledgement, completes a matching single-use provider-native authorization, or resolves one active logical installation from a stable remote tenant/resource identifier. Unknown verified updates are acknowledged as ignored; raw bodies and application credentials are never persisted. Discord interaction acknowledgements are returned only after Ed25519 signature verification.
      security: []
      parameters:
        - name: provider_key
          in: path
          required: true
          schema:
            type: string
            pattern: ^[a-z][a-z0-9_]*$
            minLength: 2
            maxLength: 50
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              maxProperties: 256
              additionalProperties: true
      responses:
        "200":
          description: Provider handshake or interaction acknowledgement returned synchronously after signature verification
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationWebhookSynchronousResponse"
        "202":
          description: Verified authorization, delivery, replay, or irrelevant update acknowledged
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationWebhookReceipt"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/integrations/sentry/options/bindings:
    get:
      tags:
        - Integrations
      operationId: listSentryNativeBindingOptions
      summary: List Sentry-native Assign Project binding options
      description: Signed Sentry App component callback. Returns at most 20 active bindings matching the installation and Sentry Project; no browser session or customer-entered webhook URL is used.
      security: []
      parameters:
        - name: Sentry-Hook-Signature
          in: header
          required: false
          schema:
            type: string
            maxLength: 128
        - name: Sentry-App-Signature
          in: header
          required: false
          schema:
            type: string
            maxLength: 128
        - name: installationId
          in: query
          required: true
          schema:
            type: string
            minLength: 1
            maxLength: 512
        - name: projectSlug
          in: query
          required: true
          schema:
            type: string
            pattern: ^[A-Za-z0-9][A-Za-z0-9_-]{0,99}$
        - name: query
          in: query
          required: false
          schema:
            type: string
            maxLength: 200
      responses:
        "200":
          description: Matching binding options
          content:
            application/json:
              schema:
                type: array
                maxItems: 20
                items:
                  $ref: "#/components/schemas/SentryNativeOption"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
  /api/v1/integrations/sentry/options/tasks:
    get:
      tags:
        - Integrations
      operationId: listSentryNativeTaskOptions
      summary: List Sentry-native Assign Task options
      description: Signed Sentry App component callback. Returns at most 20 active Tasks from the explicitly selected Project binding.
      security: []
      parameters:
        - name: Sentry-Hook-Signature
          in: header
          required: false
          schema:
            type: string
            maxLength: 128
        - name: Sentry-App-Signature
          in: header
          required: false
          schema:
            type: string
            maxLength: 128
        - name: installationId
          in: query
          required: true
          schema:
            type: string
            minLength: 1
            maxLength: 512
        - name: dependentData
          in: query
          required: true
          description: JSON object containing the selected binding_id.
          schema:
            type: string
            minLength: 2
            maxLength: 2048
        - name: query
          in: query
          required: false
          schema:
            type: string
            maxLength: 200
      responses:
        "200":
          description: Matching Task options
          content:
            application/json:
              schema:
                type: array
                maxItems: 20
                items:
                  $ref: "#/components/schemas/SentryNativeOption"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
  /api/v1/integrations/sentry/issues/create:
    post:
      tags:
        - Integrations
      operationId: createSentryNativeIssueTask
      summary: Create and link an Assign Task from Sentry
      description: Signed Sentry issue-link callback. Creates at most one Task per installation, Sentry issue, and bound Assign Project, stores bounded Sentry provenance, and returns the canonical Assign Task link.
      security: []
      parameters:
        - name: Sentry-Hook-Signature
          in: header
          required: false
          schema:
            type: string
            maxLength: 128
        - name: Sentry-App-Signature
          in: header
          required: false
          schema:
            type: string
            maxLength: 128
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SentryNativeIssueRequest"
      responses:
        "201":
          description: Assign Task created and linked
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SentryNativeIssueResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/integrations/sentry/issues/link:
    post:
      tags:
        - Integrations
      operationId: linkSentryNativeIssueTask
      summary: Link a Sentry issue to an Assign Task
      description: Signed Sentry issue-link callback. Links only to an active Task in the explicitly selected Project binding.
      security: []
      parameters:
        - name: Sentry-Hook-Signature
          in: header
          required: false
          schema:
            type: string
            maxLength: 128
        - name: Sentry-App-Signature
          in: header
          required: false
          schema:
            type: string
            maxLength: 128
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SentryNativeIssueRequest"
      responses:
        "200":
          description: Existing Assign Task linked
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SentryNativeIssueResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/integrations/sentry/alert-action:
    post:
      tags:
        - Integrations
      operationId: validateSentryNativeAlertAction
      summary: Validate an Assign Sentry alert action
      description: Signed Sentry App callback that accepts the alert action only when its selected binding has an enabled alert-to-Task behavior.
      security: []
      parameters:
        - name: Sentry-Hook-Signature
          in: header
          required: false
          schema:
            type: string
            maxLength: 128
        - name: Sentry-App-Signature
          in: header
          required: false
          schema:
            type: string
            maxLength: 128
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SentryNativeAlertActionRequest"
      responses:
        "200":
          description: Alert action configuration accepted
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required:
                  - valid
                properties:
                  valid:
                    type: boolean
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
  /api/v1/tasks/{task_id}/rich-entities:
    get:
      tags:
        - Integrations
      operationId: listTaskRichEntities
      summary: List live external entities linked to a Task
      description: |
        Returns bounded, declarative provider snapshots already persisted by
        Assign. The read path never fans out to provider APIs. Malformed or
        unsupported snapshots are omitted, and every external navigation
        target is an HTTPS URL.
      security:
        - browserSession: []
        - nativeAccessToken: []
      parameters:
        - $ref: "#/components/parameters/TaskId"
        - $ref: "#/components/parameters/Cursor"
        - name: limit
          in: query
          required: false
          description: Maximum Rich Entities to return.
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
      responses:
        "200":
          description: Task-linked Rich Entities
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RichEntityList"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/task-relations/{relation_id}:
    patch:
      tags:
        - Tasks
      operationId: updateTaskRelation
      summary: Change a Task relation type
      description: Changes only the relation type. The related Tasks cannot be edited or replaced by this operation.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/RelationId"
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TaskRelationUpdateRequest"
      responses:
        "200":
          description: Updated relation
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TaskRelation"
        "400":
          $ref: "#/components/responses/BadRequest"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
    delete:
      tags:
        - Tasks
      operationId: deleteTaskRelation
      summary: Delete a Task relation
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/RelationId"
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/IdempotencyKey"
      responses:
        "204":
          description: Relation deleted or already absent.
        "400":
          $ref: "#/components/responses/BadRequest"
        "403":
          $ref: "#/components/responses/Forbidden"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/tasks/{task_id}:
    get:
      tags:
        - Tasks
      operationId: getTask
      summary: Read a Task
      security:
        - browserSession: []
        - nativeAccessToken: []
        - bearerToken: []
        - developerAccessToken: []
      parameters:
        - $ref: "#/components/parameters/TaskId"
      responses:
        "200":
          description: The Task.
          headers:
            ETag:
              $ref: "#/components/headers/ETag"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Task"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
    patch:
      tags:
        - Tasks
      operationId: updateTask
      summary: Update a Task
      description: |
        Canonical content with an unsupported schema or an attempted
        schema 2-to-1 downgrade returns 409 document_schema_version_unsupported.
        Restoring an older Document revision retains an existing schema 2 floor.
        Browser callers require the session-bound CSRF header. Bearer callers
        omit cookies and CSRF; all callers retain identical authorization,
        idempotency and revision preconditions.
        Applies an optimistic compare-and-swap on the accepted core Task
        fields. Every field is optional; an omitted field keeps its current
        value, and an explicit `assignee_actor_id: null` clears the
        assignee. `If-Match` must carry the revision last observed by the
        client. A Task with any relation, including a parent/subtask relation,
        must be unlinked before its `project_id` can change; the API returns
        `422 task_has_relations` rather than creating a cross-Project edge.
      security:
        - browserSession: []
        - nativeAccessToken: []
        - bearerToken: []
        - developerAccessToken: []
      parameters:
        - $ref: "#/components/parameters/TaskId"
        - $ref: "#/components/parameters/BrowserCSRFToken"
        - $ref: "#/components/parameters/IdempotencyKey"
        - $ref: "#/components/parameters/IfMatch"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TaskUpdateRequest"
      responses:
        "200":
          description: The updated Task.
          headers:
            ETag:
              $ref: "#/components/headers/ETag"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Task"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "422":
          description: The requested Project move would violate a Task relation invariant.
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
    delete:
      tags:
        - Tasks
      operationId: archiveTask
      summary: Archive a Task
      description: |
        Archives a Task with optimistic concurrency. The current Task revision
        must be supplied in `If-Match`; retries with the same idempotency key
        replay the original response.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/TaskId"
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/IfMatch"
        - $ref: "#/components/parameters/IdempotencyKey"
      responses:
        "200":
          description: The archived Task, including a reversible command handle when state changed.
          headers:
            ETag:
              $ref: "#/components/headers/ETag"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Task"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "410":
          description: The Task has already been purged and cannot be archived.
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/commands/{command_id}/undo:
    post:
      tags:
        - Commands
      operationId: undoCommand
      summary: Undo a reversible command
      description: |
        Executes the server-owned inverse as a new authorized mutation. The
        command must belong to the current actor and Workspace, remain within
        its undo window, and still own the fields it changed. Unrelated edits
        do not block reversal; a later edit to the same field returns
        `409 command_conflict`.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/CommandId"
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/IdempotencyKey"
      responses:
        "200":
          description: Command undone.
          headers:
            ETag:
              $ref: "#/components/headers/ETag"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ReversibleCommandExecution"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/commands/{command_id}/redo:
    post:
      tags:
        - Commands
      operationId: redoCommand
      summary: Redo a previously undone command
      description: Executes the original server-owned forward mutation with the same ownership, expiry, permission, and field-aware conflict checks as Undo.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/CommandId"
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/IdempotencyKey"
      responses:
        "200":
          description: Command redone.
          headers:
            ETag:
              $ref: "#/components/headers/ETag"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ReversibleCommandExecution"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/tasks/{task_id}/description-revisions:
    get:
      tags:
        - Tasks
      operationId: listTaskDescriptionRevisions
      summary: List Task description revisions
      description: |
        Returns immutable Task-description snapshots newest first. The history
        begins with the baseline captured when revision retention was enabled;
        older content is never reconstructed or implied.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/TaskId"
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/PageLimit"
      responses:
        "200":
          description: A bounded newest-first description-revision page.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TaskDescriptionRevisionList"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/tasks/{task_id}/description-revisions/{revision}/restore:
    post:
      tags:
        - Tasks
      operationId: restoreTaskDescriptionRevision
      summary: Restore a Task description revision
      description: |
        Copies one retained description snapshot into a new Task revision.
        The source snapshot remains immutable, and `If-Match` prevents a
        restore from overwriting a newer Task change.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/TaskId"
        - name: revision
          in: path
          required: true
          schema:
            type: integer
            minimum: 1
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/IfMatch"
        - $ref: "#/components/parameters/IdempotencyKey"
      responses:
        "200":
          description: The Task with the restored description in a new revision.
          headers:
            ETag:
              $ref: "#/components/headers/ETag"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Task"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/tasks/{task_id}/trash:
    post:
      tags:
        - Tasks
      operationId: trashTask
      summary: Move a Task to trash
      description: |
        Removes the Task from active collections, search, notification delivery,
        reminders, recurrence, and integration writes. Content and associations
        remain recoverable until the fixed 30-day retention clock expires.
        Repeating the desired-state command at the same revision is unchanged.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/TaskId"
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/IfMatch"
        - $ref: "#/components/parameters/IdempotencyKey"
      responses:
        "200":
          description: The trashed Task and its purge eligibility instant.
          headers:
            ETag:
              $ref: "#/components/headers/ETag"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Task"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "410":
          description: The Task has already been purged and cannot be changed.
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/tasks/{task_id}/restore:
    post:
      tags:
        - Tasks
      operationId: restoreTask
      summary: Restore an archived or trashed Task
      description: |
        Restores a reversible Task to active state after revalidating its current
        Project, Status, assignee, caller access, and revision. A conflict must be
        remediated before retry. A purged tombstone cannot be restored.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/TaskId"
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/IfMatch"
        - $ref: "#/components/parameters/IdempotencyKey"
      responses:
        "200":
          description: The active Task, or the unchanged Task when it was already active.
          headers:
            ETag:
              $ref: "#/components/headers/ETag"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Task"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "410":
          description: The Task has already been purged and cannot be restored.
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/tasks/{task_id}/duplicate:
    post:
      tags:
        - Tasks
      operationId: duplicateTask
      summary: Duplicate a Task
      description: |
        Creates a new independent Task from an active source Task. Title,
        description, and priority are always copied. Callers explicitly choose
        whether to copy the current assignee, due date, milestone, labels,
        attachment links, and the active subtask tree. Comments, Activity,
        notification history, assignment periods, time entries, subscriptions,
        and integration execution history are never copied.

        Cross-Project duplication maps Statuses by semantic category unless an
        explicit destination Status is supplied, keeps Workspace labels, maps
        Project labels by normalized name, and reports optional resources that
        cannot be copied in `issues`. Milestones remain Project-local. The
        command copies at most 25 active descendants and 100 clean attachment
        links per copied Task; it never duplicates stored attachment objects or
        storage quota. `If-Match` protects the source revision, and retries with
        the same idempotency key replay the original result.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/TaskId"
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/IfMatch"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TaskDuplicateRequest"
      responses:
        "201":
          description: The new root Task, bounded copy counts, and any optional resources that were skipped.
          headers:
            ETag:
              $ref: "#/components/headers/ETag"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TaskDuplicateResult"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          description: The source revision is stale, the source is inactive, or the destination Project is unavailable.
        "410":
          description: The source Task has been purged.
        "422":
          description: No compatible destination Status exists or a requested copy exceeds a bounded command limit.
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/tasks/{task_id}/assignments:
    get:
      tags:
        - Tasks
      operationId: listTaskAssignments
      summary: List a Task's assignment history
      description: Returns immutable primary-assignee periods newest first. Imported periods can have an unknown assignment instant.
      security:
        - browserSession: []
        - nativeAccessToken: []
      parameters:
        - $ref: "#/components/parameters/TaskId"
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/PageLimit"
      responses:
        "200":
          description: A bounded page of assignment periods.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TaskAssignmentList"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/tasks/{task_id}/participants:
    get:
      tags:
        - Tasks
      operationId: listTaskParticipants
      summary: List factual Task participants
      description: Returns the Task creator, current primary assignee, and previous primary assignees without implying subscription or notification state.
      security:
        - browserSession: []
        - nativeAccessToken: []
      parameters:
        - $ref: "#/components/parameters/TaskId"
        - $ref: "#/components/parameters/PageLimit"
      responses:
        "200":
          description: A bounded participant projection.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TaskParticipantList"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/tasks/{task_id}/subscription:
    get:
      tags:
        - Tasks
      operationId: getTaskSubscription
      summary: Get the current user's Task subscription
      description: |
        Returns the caller's revisioned relationship, or a null subscription
        when no automatic or manual decision exists. Reading or mutating this
        relationship never grants Task access.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/TaskId"
      responses:
        "200":
          description: Current caller subscription state.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TaskSubscriptionEnvelope"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
    put:
      tags:
        - Tasks
      operationId: followTask
      summary: Follow a Task
      description: Idempotently sets the caller's manual subscription state to `following`. A manual state is authoritative over later automatic creator, assignee, commenter, or mention triggers.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/TaskId"
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/IdempotencyKey"
      responses:
        "200":
          description: Following state.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TaskSubscription"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
    delete:
      tags:
        - Tasks
      operationId: unfollowTask
      summary: Unfollow a Task
      description: Idempotently records an explicit `unfollowed` state. The row and prior notification history remain retained.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/TaskId"
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/IdempotencyKey"
      responses:
        "200":
          description: Unfollowed state.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TaskSubscription"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/tasks/{task_id}/subscription/mute:
    put:
      tags:
        - Tasks
      operationId: muteTask
      summary: Mute a Task
      description: Idempotently suppresses Task subscription delivery immediately without erasing relationship or notification history.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/TaskId"
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/IdempotencyKey"
      responses:
        "200":
          description: Muted state.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TaskSubscription"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
    delete:
      tags:
        - Tasks
      operationId: unmuteTask
      summary: Unmute a Task
      description: Idempotently restores the caller's manual state to `following`.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/TaskId"
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/IdempotencyKey"
      responses:
        "200":
          description: Following state.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TaskSubscription"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/tasks/{task_id}/subscribers:
    get:
      tags:
        - Tasks
      operationId: listTaskSubscribers
      summary: List current Task followers
      description: Returns only active, currently authorized user Actors whose effective state is `following`. Muted, unfollowed, removed, suspended, and inaccessible identities are excluded.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/TaskId"
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/PageLimit"
      responses:
        "200":
          description: A newest-first bounded follower page.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TaskSubscriberList"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/tasks/{task_id}/attachments:
    get:
      tags:
        - Tasks
      operationId: listTaskAttachments
      summary: List Task attachments
      description: Returns the newest 100 linked files. Optional attachment_id values select up to 100 exact linked targets regardless of age; deleted, unlinked and private Discuss files are omitted after parent authorization.
      security:
        - browserSession: []
        - nativeAccessToken: []
      parameters:
        - $ref: "#/components/parameters/TaskId"
        - $ref: "#/components/parameters/AttachmentTargetIds"
      responses:
        "200":
          description: Task attachments
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AttachmentList"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
    post:
      tags:
        - Tasks
      operationId: attachTaskAttachment
      summary: Attach a completed upload to a Task
      security:
        - browserSession: []
        - nativeAccessToken: []
      parameters:
        - $ref: "#/components/parameters/TaskId"
        - $ref: "#/components/parameters/BrowserCSRFToken"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AttachmentLinkRequest"
      responses:
        "201":
          description: Attached
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Attachment"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/tasks/{task_id}/start:
    post:
      tags:
        - Tasks
      operationId: startTask
      summary: Start and assign a Task to the calling Actor
      description: |
        Transitions the Task to the Project's preferred in-progress Status and
        assigns it to the calling Actor in one idempotent, revision-checked,
        audited transaction. A previous assignment period is closed rather
        than erased. `reassigned` indicates that the caller replaced another
        active assignee without disclosing that Actor through this command.
        This server command never changes local Git state.
      security:
        - browserSession: []
        - nativeAccessToken: []
        - bearerToken: []
        - developerAccessToken: []
      parameters:
        - $ref: "#/components/parameters/TaskId"
        - $ref: "#/components/parameters/BrowserCSRFToken"
        - $ref: "#/components/parameters/IdempotencyKey"
        - $ref: "#/components/parameters/IfMatch"
      responses:
        "200":
          description: Started Task and reassignment outcome.
          headers:
            ETag:
              $ref: "#/components/headers/ETag"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TaskStartResult"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/tasks/{task_id}/comments:
    get:
      tags:
        - Comments
      operationId: listTaskComments
      summary: List Task comments
      description: |
        Returns one page ordered by creation time, with the comment
        identifier as tie-breaker. Deleted comments are returned inline as
        tombstones — identity, author, and timestamps without a body — so
        client-side ordering and counts stay stable.
      security:
        - browserSession: []
        - bearerToken: []
        - developerAccessToken: []
        - nativeAccessToken: []
      parameters:
        - $ref: "#/components/parameters/TaskId"
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/PageLimit"
        - name: from_number
          in: query
          required: false
          description: Creation-order window start including tombstones. Zero selects the newest window. Mutually exclusive with cursor; omitted preserves cursor pagination.
          schema:
            type: integer
            format: int64
            minimum: 0
            maximum: 1000000
      responses:
        "200":
          description: A page of comments.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CommentList"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
    post:
      tags:
        - Comments
      operationId: createComment
      summary: Add a comment to a Task
      description: |
        Canonical content with an unsupported schema or an attempted
        schema 2-to-1 downgrade returns 409 document_schema_version_unsupported.
        Restoring an older Document revision retains an existing schema 2 floor.
        Appends a comment authored by the calling Actor. The body is
        validated against the Assign document schema allowlist and may not
        exceed 256 KiB of JSON.
      security:
        - browserSession: []
        - bearerToken: []
        - developerAccessToken: []
        - nativeAccessToken: []
      parameters:
        - $ref: "#/components/parameters/TaskId"
        - $ref: "#/components/parameters/BrowserCSRFToken"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CommentCreateRequest"
      responses:
        "201":
          description: The created comment.
          headers:
            ETag:
              $ref: "#/components/headers/ETag"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Comment"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/DocumentSchemaUnsupported"
        "413":
          $ref: "#/components/responses/PayloadTooLarge"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/comments/{comment_id}:
    get:
      tags:
        - Comments
      operationId: getComment
      summary: Read a comment
      security:
        - browserSession: []
        - bearerToken: []
        - developerAccessToken: []
        - nativeAccessToken: []
      parameters:
        - $ref: "#/components/parameters/CommentId"
      responses:
        "200":
          description: The comment, or its tombstone if it was deleted.
          headers:
            ETag:
              $ref: "#/components/headers/ETag"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Comment"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
    patch:
      tags:
        - Comments
      operationId: updateComment
      summary: Edit a comment
      description: |
        Canonical content with an unsupported schema or an attempted
        schema 2-to-1 downgrade returns 409 document_schema_version_unsupported.
        Restoring an older Document revision retains an existing schema 2 floor.
        Replaces the comment body in place. Only the authoring Actor may
        edit a comment; no Workspace role can edit another Actor's comment,
        and a deleted comment cannot be edited. The first successful edit
        sets the public `edited_at` marker. There is no user-visible
        version history: the previous body is not retrievable.
      security:
        - browserSession: []
        - bearerToken: []
        - developerAccessToken: []
        - nativeAccessToken: []
      parameters:
        - $ref: "#/components/parameters/CommentId"
        - $ref: "#/components/parameters/BrowserCSRFToken"
        - $ref: "#/components/parameters/IdempotencyKey"
        - $ref: "#/components/parameters/IfMatch"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CommentUpdateRequest"
      responses:
        "200":
          description: The edited comment.
          headers:
            ETag:
              $ref: "#/components/headers/ETag"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Comment"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "413":
          $ref: "#/components/responses/PayloadTooLarge"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
    delete:
      tags:
        - Comments
      operationId: deleteComment
      summary: Delete a comment
      description: |
        Soft-deletes the comment, leaving an addressable tombstone in its
        thread position. The body and its extracted search text are cleared
        immediately rather than merely hidden. The authoring Actor may
        delete their own comment; Workspace `owner` and `admin` may delete
        any comment as a moderation action, attributed to the deleting
        Actor. The operation is idempotent and there is no restore.
      security:
        - browserSession: []
        - bearerToken: []
        - developerAccessToken: []
        - nativeAccessToken: []
      parameters:
        - $ref: "#/components/parameters/CommentId"
        - $ref: "#/components/parameters/BrowserCSRFToken"
      responses:
        "204":
          description: The comment is deleted. Repeating the request succeeds unchanged.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/comments/{comment_id}/reactions/{reaction}:
    put:
      tags:
        - Comments
      operationId: addCommentReaction
      summary: Add the caller's reaction to a comment
      description: Idempotently adds one fixed reaction without changing the Comment body revision. The Comment must be live and its parent Task active.
      security:
        - browserSession: []
        - bearerToken: []
        - developerAccessToken: []
        - nativeAccessToken: []
      parameters:
        - $ref: "#/components/parameters/CommentId"
        - $ref: "#/components/parameters/CommentReaction"
        - $ref: "#/components/parameters/BrowserCSRFToken"
      responses:
        "200":
          description: The current Comment with reaction aggregates.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Comment"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
    delete:
      tags:
        - Comments
      operationId: removeCommentReaction
      summary: Remove the caller's reaction from a comment
      description: Idempotently removes one fixed reaction without changing the Comment body revision. The Comment must be live and its parent Task active.
      security:
        - browserSession: []
        - bearerToken: []
        - developerAccessToken: []
        - nativeAccessToken: []
      parameters:
        - $ref: "#/components/parameters/CommentId"
        - $ref: "#/components/parameters/CommentReaction"
        - $ref: "#/components/parameters/BrowserCSRFToken"
      responses:
        "200":
          description: The current Comment with reaction aggregates.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Comment"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/tasks/{task_id}/move:
    post:
      tags:
        - Tasks
      operationId: moveTask
      summary: Atomically move a Task in workflow order
      description: |
        Changes a Task's Status and shared manual order in one transaction.
        Neighbour anchors must be active Tasks in the destination Project and
        Status group. A stale revision returns `409 revision_conflict`; a
        missing, archived, or out-of-group anchor returns `409
        anchor_not_found`. Both conflicts carry the current Task so clients
        can re-anchor without an additional read.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/TaskId"
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TaskMoveRequest"
      responses:
        "200":
          description: The moved Task.
          headers:
            ETag:
              $ref: "#/components/headers/ETag"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Task"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          description: A stale Task revision or destination anchor, with the current Task representation.
          headers:
            ETag:
              $ref: "#/components/headers/ETag"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TaskMoveConflict"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/workspaces/{workspace_id}/label-definitions:
    get:
      tags:
        - Labels
      operationId: listWorkspaceLabelDefinitions
      description: Returns an ordered, cursor-bounded page of Workspace definitions. Continue with next_cursor using the same purpose and include_archived filters. Each page checks current Workspace authority and the commit-ordered Workspace journal version captured with the prior page. A change returns 409 catalog_changed; restart from page one. Native credentials require exactly one purpose=task query value. Other purposes are browser-only.
      summary: List active Workspace label definitions
      security:
        - browserSession: []
        - nativeAccessToken: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - name: purpose
          in: query
          schema:
            type: string
            enum:
              - document
              - project
              - task
        - name: include_archived
          in: query
          description: Include retained archived definitions for management surfaces.
          schema:
            type: boolean
            default: false
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/PageLimit"
      responses:
        "200":
          description: Label definitions.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LabelDefinitionList"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
    post:
      tags:
        - Labels
      operationId: createWorkspaceLabelDefinition
      summary: Create a Workspace-scoped label definition
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - $ref: "#/components/parameters/CSRFToken"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/LabelDefinitionCreateRequest"
      responses:
        "201":
          description: Created label definition.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LabelDefinition"
        "400":
          $ref: "#/components/responses/BadRequest"
        "403":
          $ref: "#/components/responses/Forbidden"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/projects/{project_id}/label-definitions:
    get:
      tags:
        - Labels
      operationId: listProjectLabelDefinitions
      summary: List a Project's local task-label definitions
      description: Returns an ordered, cursor-bounded page under current private Project read authority. Continue with next_cursor and the same include_archived filter. The cursor binds the prior page's commit-ordered Workspace journal version; a change returns 409 catalog_changed and requires a fresh first-page read.
      security:
        - browserSession: []
        - nativeAccessToken: []
      parameters:
        - $ref: "#/components/parameters/ProjectId"
        - name: include_archived
          in: query
          description: Include retained archived definitions for management surfaces.
          schema:
            type: boolean
            default: false
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/PageLimit"
      responses:
        "200":
          description: Label definitions.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LabelDefinitionList"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
    post:
      tags:
        - Labels
      operationId: createProjectLabelDefinition
      summary: Create a Project-local task-label definition
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/ProjectId"
        - $ref: "#/components/parameters/CSRFToken"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/LabelDefinitionCreateRequest"
      responses:
        "201":
          description: Created label definition.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LabelDefinition"
        "400":
          $ref: "#/components/responses/BadRequest"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/label-definitions/{label_id}:
    get:
      tags:
        - Labels
      operationId: getLabelDefinition
      summary: Read one authorized label definition
      description: Returns current or archived metadata in the active Workspace. Private Project access is required for Project-scoped definitions. Native credential admission is not included.
      security:
        - browserSession: []
      parameters:
        - name: label_id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/Identifier"
      responses:
        "200":
          description: Authorized label definition.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LabelDefinition"
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
    patch:
      tags:
        - Labels
      operationId: updateLabelDefinition
      summary: Rename, recolor, or restore a label definition
      security:
        - browserSession: []
      parameters:
        - name: label_id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/Identifier"
        - $ref: "#/components/parameters/IfMatch"
        - $ref: "#/components/parameters/CSRFToken"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/LabelDefinitionUpdateRequest"
      responses:
        "200":
          description: Updated label definition.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LabelDefinition"
        "400":
          $ref: "#/components/responses/BadRequest"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
    delete:
      tags:
        - Labels
      operationId: archiveLabelDefinition
      summary: Archive a label definition
      security:
        - browserSession: []
      parameters:
        - name: label_id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/Identifier"
        - $ref: "#/components/parameters/IfMatch"
        - $ref: "#/components/parameters/CSRFToken"
      responses:
        "204":
          description: Label definition archived.
        "400":
          $ref: "#/components/responses/BadRequest"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/{target_kind}/{target_id}/labels:
    get:
      tags:
        - Labels
      operationId: listTargetLabelAssignments
      description: Native mobile credentials may read Task assignments only (target_kind=task).
      summary: List a document, project, or task's label IDs
      security:
        - browserSession: []
        - nativeAccessToken: []
      parameters:
        - name: target_kind
          in: path
          required: true
          schema:
            type: string
            enum:
              - document
              - project
              - task
        - name: target_id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/Identifier"
      responses:
        "200":
          description: Label IDs.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LabelAssignmentReplaceRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
    put:
      tags:
        - Labels
      operationId: replaceTargetLabels
      summary: Replace a document, project, or task's complete label assignment set
      security:
        - browserSession: []
      parameters:
        - name: target_kind
          in: path
          required: true
          schema:
            type: string
            enum:
              - document
              - project
              - task
        - name: target_id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/Identifier"
        - $ref: "#/components/parameters/CSRFToken"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/LabelAssignmentReplaceRequest"
      responses:
        "204":
          description: Labels replaced.
        "400":
          $ref: "#/components/responses/BadRequest"
        "403":
          $ref: "#/components/responses/Forbidden"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/projects/{project_id}/label-overrides:
    get:
      tags:
        - Labels
      operationId: listProjectTaskLabelOverrides
      summary: List a Project's presentation overrides for Workspace Task labels
      description: |
        A sparse list: only Workspace Task labels with an active Project
        override appear. A label with no entry here is shown with its
        Workspace name and color in this Project.
        Returns up to `limit` overrides in label ID order. Continue with
        `next_cursor` while `has_more` is true. Cursors are signed and bound
        to the current Workspace and Project. Restart from the first page
        after a catalog change when a complete current view is required.
        The cursor binds the prior page's commit-ordered Workspace journal
        version. A change returns 409 catalog_changed before another page
        can be applied.
      security:
        - browserSession: []
        - nativeAccessToken: []
      parameters:
        - $ref: "#/components/parameters/ProjectId"
        - $ref: "#/components/parameters/PageLimit"
        - $ref: "#/components/parameters/Cursor"
      responses:
        "200":
          description: Project Task label overrides.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProjectTaskLabelOverrideList"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/projects/{project_id}/label-overrides/{label_id}:
    put:
      tags:
        - Labels
      operationId: setProjectTaskLabelOverride
      summary: Set this Project's presentation of a Workspace Task label
      description: |
        Upserts the Project's own name and color for a Workspace-scope Task
        label. The label's stable identity and existing Task assignments are
        unaffected; only how it is displayed within this Project changes.
        Only a Workspace-scope Task label may be overridden.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/ProjectId"
        - name: label_id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/Identifier"
        - $ref: "#/components/parameters/CSRFToken"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ProjectTaskLabelOverrideRequest"
      responses:
        "200":
          description: The Project's override for this label.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProjectTaskLabelOverride"
        "400":
          $ref: "#/components/responses/BadRequest"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
    delete:
      tags:
        - Labels
      operationId: clearProjectTaskLabelOverride
      summary: Restore the Workspace default presentation of a Task label in this Project
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/ProjectId"
        - name: label_id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/Identifier"
        - $ref: "#/components/parameters/CSRFToken"
      responses:
        "204":
          description: Override cleared; the Project now shows the Workspace default.
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/public/documents/{public_id}:
    get:
      tags:
        - Documents
      operationId: getPublicDocument
      summary: Read a published Document
      description: |
        Public access requires the owning Workspace's public-publishing billing entitlement; absence returns the same privacy-safe 404 as an unavailable identifier.
        Returns only the public-safe title and content of an unarchived
        Document. The opaque public identifier is revoked when the Document
        is unpublished or archived. This route does not disclose Workspace,
        Project, hierarchy, membership, revision, or audit information.
      security: []
      parameters:
        - name: public_id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/Identifier"
      responses:
        "200":
          description: The deliberately minimal public Document representation.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PublicDocument"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/public/documents/{public_id}/attachments/{attachment_id}/download:
    post:
      tags:
        - Documents
      operationId: authorizePublicDocumentAttachmentDownload
      summary: Authorize a published Document attachment download
      description: |
        Public access requires the owning Workspace's public-publishing billing entitlement; absence returns the same privacy-safe 404 as an unavailable identifier.
        Mints a short-lived download URL only when the attachment is owned by
        the published Document and is referenced by its current public body.
        Historical ownership alone never exposes a removed attachment.
      security: []
      parameters:
        - name: public_id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/Identifier"
        - $ref: "#/components/parameters/AttachmentId"
      responses:
        "201":
          description: Short-lived download authorization.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AttachmentDownload"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/public/documents/{public_id}/attachments/{attachment_id}/preview:
    post:
      tags:
        - Documents
      operationId: authorizePublicDocumentAttachmentPreview
      summary: Authorize a published Document attachment preview
      description: |
        Public access requires the owning Workspace's public-publishing billing entitlement; absence returns the same privacy-safe 404 as an unavailable identifier.
        Mints a short-lived cookie-less inline URL for a previewable attachment
        that is both owned by and referenced from the current public body.
      security: []
      parameters:
        - name: public_id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/Identifier"
        - $ref: "#/components/parameters/AttachmentId"
      responses:
        "201":
          description: Short-lived inline preview authorization.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AttachmentPreview"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/public/projects/{public_id}:
    get:
      tags:
        - Projects
      operationId: getPublicProject
      summary: Read a published Project status
      description: |
        Public access requires the owning Workspace's public-publishing billing entitlement; absence returns the same privacy-safe 404 as an unavailable identifier.
        Returns a deliberately minimal read-only Project status: its name,
        optional visual marker, and ordered workflow Status labels with
        aggregate Task counts. The opaque identifier is revoked on unpublish
        or archive. Workspace identity, owner identity, Task content, members,
        Documents, attachments, labels, revisions, and audit data are omitted.
      security: []
      parameters:
        - name: public_id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/Identifier"
      responses:
        "200":
          description: The public-safe Project status.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PublicProject"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/workspaces/{workspace_id}/documents:
    get:
      tags:
        - Documents
      operationId: listWorkspaceDocuments
      summary: List Workspace Documents
      description: |
        Returns one cursor-paginated page of active root Document metadata, or
        archived roots when `archived_only=true`. Title order is the default;
        `sort` selects modified-time order. Text, Project, location, scope,
        descendant, and archive selectors are applied before pagination; a
        cursor is valid only for the same selectors and sort. Content is not
        included; read it through the content operation.

        `location` classifies a Document by structure only: `workspace` is
        Documents outside every Project and `projects` is Documents inside one.
        It never describes visibility or access; `scope` remains the audience
        filter. A nested Document always shares its parent's Project, so it is
        classified by the same rule.

        `include_descendants=true` returns nested Documents as individual rows
        instead of roots only, each exactly once, with an `ancestors` path so a
        flattened result can show where it lives.
      security:
        - browserSession: []
        - nativeAccessToken: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - name: project_id
          in: query
          required: false
          description: Restrict the page to Documents owned by this Project.
          schema:
            $ref: "#/components/schemas/Identifier"
        - name: q
          in: query
          required: false
          description: Match terms across a Document title and extracted text.
          schema:
            type: string
            minLength: 1
            maxLength: 200
        - name: scope
          in: query
          required: false
          description: Restrict the page to one Document visibility scope.
          schema:
            type: string
            enum:
              - workspace
              - project
              - public
        - name: archived_only
          in: query
          required: false
          description: Return archived roots for recovery instead of active roots.
          schema:
            type: boolean
            default: false
        - name: location
          in: query
          required: false
          description: |
            Restrict the page by structural location. `workspace` returns
            Documents outside every Project; `projects` returns Documents that
            belong to a Project. Omit for both. Cannot be combined with
            `project_id` when `workspace`.
          schema:
            type: string
            enum:
              - workspace
              - projects
        - name: sort
          in: query
          required: false
          description: |
            Page order. `title` is ascending title (default), `updated_desc` is
            most recently modified first and `updated_asc` is least recently
            modified first, by each Document's own modification time. Ties
            break by identifier.
          schema:
            type: string
            enum:
              - title
              - updated_desc
              - updated_asc
            default: title
        - name: include_descendants
          in: query
          required: false
          description: Return nested Documents as individual rows, each once, with their ancestor path.
          schema:
            type: boolean
            default: false
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/PageLimit"
      responses:
        "200":
          description: A page of Documents.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DocumentList"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
    post:
      tags:
        - Documents
      operationId: createDocument
      summary: Create a Document
      description: |
        Canonical content with an unsupported schema or an attempted
        schema 2-to-1 downgrade returns 409 document_schema_version_unsupported.
        Restoring an older Document revision retains an existing schema 2 floor.
        Creates a Document with optional initial content. Omitting `content`
        creates an empty Document.
        Public Document creation and transitions to public visibility require the Workspace's
        public-publishing billing entitlement; denial returns 403 paid_workspace_required.
        Unpublishing and ordinary edits of an existing publication remain available subject to
        normal authorization. Public links are unavailable while the entitlement is absent.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/DocumentCreateRequest"
      responses:
        "201":
          description: The created Document.
          headers:
            ETag:
              $ref: "#/components/headers/ETag"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Document"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/DocumentSchemaUnsupported"
        "413":
          $ref: "#/components/responses/PayloadTooLarge"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/workspaces/{workspace_id}/documents/by-path/{document_path}:
    get:
      tags:
        - Documents
      operationId: getDocumentByPath
      summary: Resolve active Document metadata by Workspace path
      description: |
        Resolves an exact active Document path within the requested Workspace.
        Applies the same Workspace and Project read permissions as the Document
        identifier read. A missing, archived, or inaccessible Document returns 404.
        Use the returned identifier for content and subsequent Document reads.
      security:
        - browserSession: []
        - nativeAccessToken: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - name: document_path
          in: path
          required: true
          description: Exact Document path within the Workspace.
          schema:
            type: string
            minLength: 1
            maxLength: 200
      responses:
        "200":
          description: The Document metadata.
          headers:
            ETag:
              $ref: "#/components/headers/ETag"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Document"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/documents/{document_id}/links:
    get:
      tags:
        - Documents
      operationId: listDocumentWorkLinks
      summary: List Document work links
      description: Scans at most 100 associations per page and returns only currently authorized live pairs. An empty page may have a continuation. Cursors are bound to the owner and Workspace. Links confer no access.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/DocumentId"
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/PageLimit"
      responses:
        "200":
          description: Current authorized associations.
          headers:
            X-Assign-Realtime-Cursor:
              $ref: "#/components/headers/RealtimeReadCursor"
            X-Assign-Realtime-Position:
              $ref: "#/components/headers/RealtimeReadPosition"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DocumentWorkLinkList"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/tasks/{task_id}/documents:
    get:
      tags:
        - Documents
      operationId: listTaskDocuments
      summary: List Task Documents
      description: Scans at most 100 associations per page and returns only currently authorized live pairs. An empty page may have a continuation. Cursors are bound to the owner and Workspace. Links confer no access.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/TaskId"
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/PageLimit"
      responses:
        "200":
          description: Current authorized associations.
          headers:
            X-Assign-Realtime-Cursor:
              $ref: "#/components/headers/RealtimeReadCursor"
            X-Assign-Realtime-Position:
              $ref: "#/components/headers/RealtimeReadPosition"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DocumentWorkLinkList"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/milestones/{milestone_id}/documents:
    get:
      tags:
        - Documents
      operationId: listMilestoneDocuments
      summary: List Milestone Documents
      description: Scans at most 100 associations per page and returns only currently authorized live pairs. An empty page may have a continuation. Cursors are bound to the owner and Workspace. Links confer no access.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/MilestoneId"
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/PageLimit"
      responses:
        "200":
          description: Current authorized associations.
          headers:
            X-Assign-Realtime-Cursor:
              $ref: "#/components/headers/RealtimeReadCursor"
            X-Assign-Realtime-Position:
              $ref: "#/components/headers/RealtimeReadPosition"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DocumentWorkLinkList"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/documents/{document_id}/links/{target_kind}/{target_id}:
    post:
      tags:
        - Documents
      operationId: createDocumentWorkLink
      summary: Link a Document to a Task or Milestone
      description: Requires write authority for both participants. Project-owned Documents can link only within their Project. A Workspace Document can link to any authorized Project. Pair creation and removal are idempotent. Public Document visibility does not publish associations.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/DocumentId"
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/IdempotencyKey"
        - name: target_kind
          in: path
          required: true
          schema:
            type: string
            enum:
              - task
              - milestone
        - name: target_id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/Identifier"
      responses:
        "200":
          description: Created or existing association.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DocumentWorkLink"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
    delete:
      tags:
        - Documents
      operationId: deleteDocumentWorkLink
      summary: Remove a Document work link
      description: Requires write authority for both participants. Project-owned Documents can link only within their Project. A Workspace Document can link to any authorized Project. Pair creation and removal are idempotent. Public Document visibility does not publish associations.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/DocumentId"
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/IdempotencyKey"
        - name: target_kind
          in: path
          required: true
          schema:
            type: string
            enum:
              - task
              - milestone
        - name: target_id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/Identifier"
      responses:
        "204":
          description: Association removed or already absent.
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/documents/{document_id}:
    get:
      tags:
        - Documents
      operationId: getDocument
      summary: Read Document metadata
      security:
        - browserSession: []
        - nativeAccessToken: []
      parameters:
        - $ref: "#/components/parameters/DocumentId"
      responses:
        "200":
          description: The Document metadata.
          headers:
            ETag:
              $ref: "#/components/headers/ETag"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Document"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
    patch:
      tags:
        - Documents
      operationId: updateDocument
      summary: Update Document metadata
      description: |
        Applies an optimistic compare-and-swap to Document metadata. Content
        is replaced through the separate content operation so a title rename
        never competes with an autosave for the same revision.
        Public Document creation and transitions to public visibility require the Workspace's
        public-publishing billing entitlement; denial returns 403 paid_workspace_required.
        Unpublishing and ordinary edits of an existing publication remain available subject to
        normal authorization. Public links are unavailable while the entitlement is absent.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/DocumentId"
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/IdempotencyKey"
        - $ref: "#/components/parameters/IfMatch"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/DocumentUpdateRequest"
      responses:
        "200":
          description: The updated Document.
          headers:
            ETag:
              $ref: "#/components/headers/ETag"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Document"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
    delete:
      tags:
        - Documents
      operationId: archiveDocument
      summary: Archive a Document
      description: |
        Archives the Document. Archived Documents remain readable by
        identifier and continue to count against storage accounting until
        they are purged at the end of the retention window.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/DocumentId"
        - $ref: "#/components/parameters/CSRFToken"
      responses:
        "204":
          description: The Document is archived.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/documents/{document_id}/permanent:
    delete:
      tags:
        - Documents
      operationId: deleteDocument
      summary: Permanently delete a Document
      description: |
        Permanently removes an active or archived leaf Document, including its
        revision history, collaboration state and resource links. Requires the
        same Workspace and owning Project write permission as editing.
        Documents with any child (including archived children) return 409
        document_has_children. Git-bound Documents return 409 document_git_managed.
        A stale If-Match returns 409 revision_conflict. Attachment objects retain
        their existing cleanup policy. Archive and Restore use separate operations.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/DocumentId"
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/IfMatch"
      responses:
        "204":
          description: The Document is permanently deleted.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/documents/{document_id}/restore:
    post:
      tags:
        - Documents
      operationId: restoreDocument
      summary: Restore an archived Document
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/DocumentId"
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/IfMatch"
      responses:
        "200":
          description: Restored active Document
          headers:
            ETag:
              $ref: "#/components/headers/ETag"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Document"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/documents/{document_id}/attachments:
    get:
      tags:
        - Documents
      operationId: listDocumentAttachments
      summary: List Document attachments
      description: Returns the newest 100 linked files. Optional attachment_id values select up to 100 exact linked targets regardless of age; deleted, unlinked and private Discuss files are omitted after parent authorization.
      security:
        - browserSession: []
        - nativeAccessToken: []
      parameters:
        - $ref: "#/components/parameters/DocumentId"
        - $ref: "#/components/parameters/AttachmentTargetIds"
      responses:
        "200":
          description: Document attachments.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AttachmentList"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
    post:
      tags:
        - Documents
      operationId: attachDocumentAttachment
      summary: Attach a completed upload to a Document
      description: |
        Establishes durable Document ownership before content stores the
        attachment identifier. The idempotent link remains while current or
        historical revisions may reference it and is removed with Document
        purge or explicit attachment deletion.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/DocumentId"
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AttachmentLinkRequest"
      responses:
        "201":
          description: Attached.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Attachment"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/documents/{document_id}/revisions:
    get:
      tags:
        - Documents
      operationId: listDocumentRevisions
      summary: List immutable Document revisions
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/DocumentId"
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/PageLimit"
      responses:
        "200":
          description: Newest-first revision page
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DocumentRevisionList"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/documents/{document_id}/revisions/compare:
    get:
      tags:
        - Documents
      operationId: compareDocumentRevisions
      summary: Compare two immutable Document revisions
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/DocumentId"
        - name: from
          in: query
          required: true
          schema:
            type: integer
            minimum: 1
        - name: to
          in: query
          required: true
          schema:
            type: integer
            minimum: 1
      responses:
        "200":
          description: Both snapshots and the fields that changed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DocumentRevisionComparison"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/documents/{document_id}/revisions/{revision}:
    get:
      tags:
        - Documents
      operationId: getDocumentRevision
      summary: Read one immutable Document revision
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/DocumentId"
        - name: revision
          in: path
          required: true
          schema:
            type: integer
            minimum: 1
      responses:
        "200":
          description: Immutable revision snapshot
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DocumentRevision"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/documents/{document_id}/revisions/{revision}/restore:
    post:
      tags:
        - Documents
      operationId: restoreDocumentRevision
      summary: Restore a historical snapshot as a new active revision
      description: |
        The source snapshot remains immutable. The current Document revision must be supplied in `If-Match`.
        Canonical content with an unsupported schema or an attempted
        schema 2-to-1 downgrade returns 409 document_schema_version_unsupported.
        Restoring an older Document revision retains an existing schema 2 floor.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/DocumentId"
        - name: revision
          in: path
          required: true
          schema:
            type: integer
            minimum: 1
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/IfMatch"
      responses:
        "200":
          description: Newly restored active revision
          headers:
            ETag:
              $ref: "#/components/headers/ETag"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Document"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/documents/{document_id}/markdown:
    get:
      tags:
        - Documents
      operationId: exportDocumentMarkdown
      summary: Export current or historical Document content as Markdown
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/DocumentId"
        - name: revision
          in: query
          schema:
            type: integer
            minimum: 1
      responses:
        "200":
          description: Deterministic Markdown for the supported safe subset
          content:
            text/markdown:
              schema:
                type: string
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
    put:
      tags:
        - Documents
      operationId: importDocumentMarkdown
      summary: Replace Document content from Markdown
      description: Accepts bounded valid UTF-8 Markdown. Supported Assign-profile constructs become structured content; unsupported blocks and inline constructs are preserved as inert literal Markdown. Raw HTML is never executed, and tables remain literal until a versioned table format is available.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/DocumentId"
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/IfMatch"
      requestBody:
        required: true
        content:
          text/markdown:
            schema:
              type: string
              maxLength: 2097152
      responses:
        "200":
          description: Imported canonical Document content
          headers:
            ETag:
              $ref: "#/components/headers/ETag"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DocumentContent"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/DocumentContentConflict"
        "413":
          $ref: "#/components/responses/PayloadTooLarge"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/documents/{document_id}/children:
    get:
      tags:
        - Documents
      operationId: listDocumentChildren
      summary: List direct child Documents
      description: Returns one cursor-paginated page of direct children the caller may read, title-ordered unless `sort` selects modified-time order.
      security:
        - browserSession: []
        - nativeAccessToken: []
      parameters:
        - $ref: "#/components/parameters/DocumentId"
        - name: sort
          in: query
          required: false
          description: Sibling order. `title` (default), `updated_desc` or `updated_asc`, matching the workspace Document collection.
          schema:
            type: string
            enum:
              - title
              - updated_desc
              - updated_asc
            default: title
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/PageLimit"
      responses:
        "200":
          description: A page of direct child Document metadata.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DocumentList"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/documents/{document_id}/content:
    get:
      tags:
        - Documents
      operationId: getDocumentContent
      summary: Read versioned Document content
      security:
        - browserSession: []
        - nativeAccessToken: []
      parameters:
        - $ref: "#/components/parameters/DocumentId"
      responses:
        "200":
          description: The current content and the revision it belongs to.
          headers:
            ETag:
              $ref: "#/components/headers/ETag"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DocumentContent"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
    put:
      tags:
        - Documents
      operationId: replaceDocumentContent
      summary: Replace versioned Document content
      description: |
        Canonical content with an unsupported schema or an attempted
        schema 2-to-1 downgrade returns 409 document_schema_version_unsupported.
        Restoring an older Document revision retains an existing schema 2 floor.
        Replaces Document content through versioned optimistic autosave. The
        client sends the revision it edited in `If-Match`. A stale revision
        is refused with `409` carrying the **current server content**, so the
        client can keep its local draft and offer compare, reload, or retry
        without a second request. Success means durably acknowledged.

        This is not a collaborative editing protocol: no CRDT, operational
        transform, presence, or admission behaviour is implied.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/DocumentId"
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/IfMatch"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/DocumentContentReplaceRequest"
      responses:
        "200":
          description: The stored content and its new revision.
          headers:
            ETag:
              $ref: "#/components/headers/ETag"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DocumentContent"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/DocumentContentConflict"
        "413":
          $ref: "#/components/responses/PayloadTooLarge"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/tasks/{task_id}/collaboration-sessions:
    post:
      tags:
        - Tasks
      operationId: admitTaskCollaborationSession
      summary: Admit an independent Task collaboration-presence lease
      description: |
        Evaluates the caller's current Task permission and creates an
        independent session for this browser client. A caller with read-only
        access receives role `viewer`; a caller who may edit receives `editor`.
        The opaque `admission_token` lasts five minutes, is scoped to this
        Task, actor, and session, and authorizes nothing on this HTTP API.
        Each admission has its own token and exclusive connection lease, so
        another tab cannot invalidate it. The connected relay renews the
        lease; disconnected sessions expire through bounded cleanup.

        The lease admits the browser-only Task description relay. Raw Yjs
        updates, checkpoints and caret awareness are not REST or SDK payloads.
        Task creation drafts, Comments, archived and trashed Tasks are excluded.

        X-Assign-Document-Schema defaults to 1. An editable schema 2 admission
        promotes only this resource's envelope through its normal revision
        transition, preserving opaque updates, checkpoints and replica generation.
        Existing schema 1 resources continue to accept schema 1 clients.
        Once promoted, schema 1 admission and already admitted schema 1 writes
        fail with document_schema_version_unsupported. Invalid header values
        fail with document_schema_version_unsupported. Project permissions remain authoritative.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/TaskId"
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/DocumentSchemaCapability"
      responses:
        "201":
          description: The independently admitted collaboration-presence lease.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DocumentCollaborationSession"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/DocumentSchemaUnsupported"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/tasks/{task_id}/collaboration-sessions/{session_id}:
    delete:
      tags:
        - Tasks
      operationId: endTaskCollaborationSession
      summary: End the caller's Task collaboration-presence lease
      description: |
        Ends only the caller's own session. Repeating a successful end remains
        successful. A nonexistent or somebody else's session returns `404` so
        the operation cannot disclose collaborator presence. No Workspace
        event or Activity item is written.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/TaskId"
        - name: session_id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/Identifier"
        - $ref: "#/components/parameters/CSRFToken"
      responses:
        "204":
          description: The session is ended or was already ended by this caller.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/documents/{document_id}/collaboration-sessions:
    post:
      tags:
        - Documents
      operationId: admitDocumentCollaborationSession
      summary: Admit an independent Document collaboration-presence lease
      description: |
        Evaluates the caller's current Document permission and creates an
        independent session for this browser client. A caller with read-only
        access receives role `viewer`; a caller who may edit receives `editor`.
        The opaque `admission_token` lasts five minutes, is scoped to this
        Document, actor, and session, and authorizes nothing on this HTTP API.
        Each admission has its own token and exclusive connection lease, so
        another tab cannot invalidate it. The connected relay renews the
        lease; disconnected sessions expire through bounded cleanup.

        The lease admits the browser-only durable Yjs relay. Raw updates,
        checkpoints and awareness are not REST or SDK payloads. Resource schema
        capability is checked at admission and again for each update/checkpoint.

        X-Assign-Document-Schema defaults to 1. An editable schema 2 admission
        promotes only this resource's envelope through its normal revision
        transition, preserving opaque updates, checkpoints and replica generation.
        Existing schema 1 resources continue to accept schema 1 clients.
        Once promoted, schema 1 admission and already admitted schema 1 writes
        fail with document_schema_version_unsupported. Invalid header values
        fail with document_schema_version_unsupported. Project permissions remain authoritative.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/DocumentId"
        - $ref: "#/components/parameters/CSRFToken"
        - $ref: "#/components/parameters/DocumentSchemaCapability"
      responses:
        "201":
          description: The independently admitted collaboration-presence lease.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DocumentCollaborationSession"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/DocumentSchemaUnsupported"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/documents/{document_id}/collaboration-sessions/{session_id}:
    delete:
      tags:
        - Documents
      operationId: endDocumentCollaborationSession
      summary: End the caller's Document collaboration-presence lease
      description: |
        Ends only the caller's own session. Repeating a successful end remains
        successful. A nonexistent or somebody else's session returns `404` so
        the operation cannot disclose collaborator presence. No Workspace
        event or Activity item is written.
      security:
        - browserSession: []
      parameters:
        - $ref: "#/components/parameters/DocumentId"
        - name: session_id
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/Identifier"
        - $ref: "#/components/parameters/CSRFToken"
      responses:
        "204":
          description: The session is ended or was already ended by this caller.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/attachment-uploads:
    post:
      tags:
        - Attachments
      operationId: reserveAttachmentUpload
      summary: Reserve a direct upload
      description: |
        Atomically reserves the declared byte count against the Workspace's
        pooled storage entitlement and returns a short-lived presigned URL
        bound to one method, one object key, and the reserved length. File
        bytes never pass through the Assign API.

        The reservation expires if it is not completed. Expired, cancelled,
        and abandoned reservations release their quota.

        Associating a completed attachment with a parent Task, comment, or
        Document is deliberately **not** part of this contract: attachment
        parent cardinality is an open decision and will be contracted with
        it rather than inferred from an implementation.
      security:
        - browserSession: []
        - nativeAccessToken: []
      parameters:
        - $ref: "#/components/parameters/BrowserCSRFToken"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AttachmentUploadRequest"
      responses:
        "201":
          description: The reservation and its presigned upload target.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AttachmentUpload"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "413":
          $ref: "#/components/responses/PayloadTooLarge"
        "422":
          $ref: "#/components/responses/QuotaExceeded"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/attachment-uploads/{upload_id}/complete:
    post:
      tags:
        - Attachments
      operationId: completeAttachmentUpload
      summary: Complete a reserved upload
      description: |
        Verifies the uploaded object, streams it through the private malware
        scanner, and converts reserved bytes into stored bytes. Completion checks object existence, the exact observed size,
        the expected object key, and the SHA-256 digest, and decides the
        content type from the object's leading bytes — never from the
        client-declared type, which is advisory and is discarded.

        Detection and the filename extension are independent grounds for
        denial: an object whose detected type is on the execution-risk
        denylist is rejected whatever it is named, and a denied extension is
        rejected whatever the bytes contain. Neither can permit what the
        other rejects. SVG is retained as a download-only attachment: it is
        never eligible for inline display. Scanner failures fail completion
        closed. A detected malicious attachment is retained only in private
        quarantine with `scan_state: quarantined`; it cannot be linked,
        previewed, or downloaded.
      security:
        - browserSession: []
        - nativeAccessToken: []
      parameters:
        - $ref: "#/components/parameters/UploadId"
        - $ref: "#/components/parameters/BrowserCSRFToken"
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AttachmentUploadCompleteRequest"
      responses:
        "201":
          description: The stored attachment.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Attachment"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "422":
          $ref: "#/components/responses/AttachmentRejected"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/attachment-uploads/{upload_id}:
    delete:
      tags:
        - Attachments
      operationId: cancelAttachmentUpload
      summary: Cancel a reserved upload
      description: |
        Releases the reservation and its quota. The operation is idempotent
        and succeeds for an already-cancelled or already-expired reservation.
      security:
        - browserSession: []
        - nativeAccessToken: []
      parameters:
        - $ref: "#/components/parameters/UploadId"
        - $ref: "#/components/parameters/BrowserCSRFToken"
      responses:
        "204":
          description: The reservation is released.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/attachments/{attachment_id}:
    get:
      tags:
        - Attachments
      operationId: getAttachment
      summary: Read attachment metadata
      security:
        - browserSession: []
        - nativeAccessToken: []
      parameters:
        - $ref: "#/components/parameters/AttachmentId"
      responses:
        "200":
          description: The attachment metadata.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Attachment"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
    delete:
      tags:
        - Attachments
      operationId: deleteAttachment
      summary: Delete an attachment
      description: |
        Soft-deletes the attachment. Stored bytes continue to count against
        the storage entitlement until the object is permanently purged at the
        end of the retention window; deletion is never triggered
        automatically by quota exhaustion.
      security:
        - browserSession: []
        - nativeAccessToken: []
      parameters:
        - $ref: "#/components/parameters/AttachmentId"
        - $ref: "#/components/parameters/BrowserCSRFToken"
      responses:
        "204":
          description: The attachment is deleted.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/attachments/{attachment_id}/download:
    post:
      tags:
        - Attachments
      operationId: authorizeAttachmentDownload
      summary: Authorize a direct download
      description: |
        Authorizes the caller against the attachment's Workspace at request
        time and mints a short-lived presigned download URL bound to one
        method and one object key. Possession of an attachment identifier or
        an expired URL never grants access.

        The URL is minted per request and must never be stored in Document or
        comment content, an event payload, a notification, or a webhook. The
        object is served from a non-Assign origin with
        `Content-Disposition: attachment` and `X-Content-Type-Options:
        nosniff`; attachments are never rendered inline in an Assign origin.

        Only an attachment whose `scan_state` is `clean` can receive this
        credential. This operation is `POST` because it mints a credential; it is not a
        cacheable read.
      security:
        - browserSession: []
        - nativeAccessToken: []
      parameters:
        - $ref: "#/components/parameters/AttachmentId"
        - $ref: "#/components/parameters/BrowserCSRFToken"
      responses:
        "201":
          description: A short-lived presigned download target.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AttachmentDownload"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
  /api/v1/attachments/{attachment_id}/preview:
    post:
      tags:
        - Attachments
      operationId: authorizeAttachmentPreview
      summary: Authorize an inline attachment preview
      description: |
        Authorizes the caller against the attachment's Workspace at request
        time and mints a short-lived presigned URL for browser display. The
        object is served from the cookie-less object-storage origin with an
        inline disposition, `X-Content-Type-Options: nosniff`, and the
        server-detected content type.

        Only clean PNG, JPEG, GIF, WebP, AVIF, and PDF attachments are previewable.
        SVG and every other type remain download-only. The URL is a credential,
        is minted per request, and must never be stored in content, events,
        notifications, or webhooks.

        This operation is `POST` because it mints a credential; it is not a
        cacheable read.
      security:
        - browserSession: []
        - nativeAccessToken: []
      parameters:
        - $ref: "#/components/parameters/AttachmentId"
        - $ref: "#/components/parameters/BrowserCSRFToken"
      responses:
        "201":
          description: A short-lived presigned inline preview target.
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AttachmentPreview"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/MaintenanceUnavailable"
components:
  securitySchemes:
    browserSession:
      type: apiKey
      in: cookie
      name: __Host-assign_session
      description: Secure, HttpOnly, host-only browser session cookie. Successful browser-session creation sends both `Expires` and a positive `Max-Age` to make the 30-day absolute persistence deadline explicit; invalid-session and logout responses clear it with `Max-Age=0`.
    bearerToken:
      type: http
      scheme: bearer
      bearerFormat: Assign access token
      description: |
        A revocable, scoped access token issued for non-interactive clients.
        The token is audience-bound to the HTTPS API host; clients must not
        send it to another host or place it in a URL. Discuss operations
        additionally require the dedicated `assign:discuss` scope.
    developerAccessToken:
      type: http
      scheme: bearer
      bearerFormat: Assign developer-client OAuth access token
      description: |
        A 15-minute opaque API token issued only to a registered first-party
        developer client (`assign-cli`, `assign-jetbrains`, or `assign-vscode`)
        through loopback
        Authorization Code + PKCE. Each client ID owns a distinct grant and
        refresh family. CLI grants receive `assign:read`, `assign:write`, and
        `assign:discuss`; VS Code and JetBrains grants receive only
        `assign:read` and `assign:write`. It is not a
        personal API token, browser session, native token, or MCP token. The
        co-versioned terminal Discuss client requires `assign:discuss`.
    nativeAccessToken:
      type: http
      scheme: bearer
      bearerFormat: Assign native mobile access token
      description: |
        A 15-minute opaque token issued only by the native mobile OAuth token
        endpoint. It is bound to an Assign Mobile credential and API audience,
        and is not interchangeable with a browser session, personal API token,
        CLI token, or MCP OAuth token. The server rechecks current membership,
        capability, resource visibility, lifecycle, and entitlement on every
        request.
  parameters:
    AttachmentTargetIds:
      name: attachment_id
      in: query
      required: false
      style: form
      explode: true
      description: Exact attachment targets linked to this authorized parent, independent of the newest-file window. Repeat the query key for multiple IDs. At most 100 values; invalid or empty values return 400. Omit for the ordinary newest-100 shelf.
      schema:
        type: array
        minItems: 1
        maxItems: 100
        items:
          type: string
          format: uuid
    WorkspaceRealtimeDerivedBaseline:
      name: X-Assign-Realtime-Baseline
      in: header
      required: false
      description: Upcoming browser-session derived read. Set to '1' to capture an authorized Workspace replay boundary before remote Knowledge retrieval. Core joins occur in newer snapshots; the result can still report index lag. Apply retained results before acknowledging the socket checkpoint. Failed reads return no cursor.
      schema:
        type: string
        enum:
          - "1"
    WorkspaceRealtimeBaseline:
      name: X-Assign-Realtime-Baseline
      in: header
      required: false
      description: Upcoming browser-session canonical read. Set to '1' to capture the authorized response and a signed Workspace replay cursor in one snapshot. Apply every required retained read before acknowledging a socket checkpoint. Failed or incomplete reads return no cursor.
      schema:
        type: string
        enum:
          - "1"
    DocumentSchemaCapability:
      name: X-Assign-Document-Schema
      in: header
      required: false
      description: Highest writable document schema supported by this client. Missing means schema 1. Editable schema 2 admission atomically promotes the resource envelope without resetting its collaboration state. Older admissions are refused after promotion.
      schema:
        type: integer
        enum:
          - 1
          - 2
        default: 1
    MobileCredentialId:
      name: credential_id
      in: path
      required: true
      schema:
        $ref: "#/components/schemas/Identifier"
    BrowserCSRFToken:
      name: X-CSRF-Token
      in: header
      required: false
      description: Required when this operation uses `browserSession`; native bearer callers omit it because no ambient cookie is sent.
      schema:
        type: string
        minLength: 1
        maxLength: 200
    PersonalApiTokenId:
      name: token_id
      in: path
      required: true
      schema:
        $ref: "#/components/schemas/Identifier"
    McpGrantId:
      name: grant_id
      in: path
      required: true
      schema:
        $ref: "#/components/schemas/Identifier"
    McpServiceCredentialId:
      name: credential_id
      in: path
      required: true
      schema:
        $ref: "#/components/schemas/Identifier"
    RelationId:
      name: relation_id
      in: path
      required: true
      schema:
        $ref: "#/components/schemas/Identifier"
    CommandId:
      name: command_id
      in: path
      required: true
      schema:
        $ref: "#/components/schemas/Identifier"
    MilestoneId:
      name: milestone_id
      in: path
      required: true
      schema:
        $ref: "#/components/schemas/Identifier"
    Cursor:
      name: cursor
      in: query
      required: false
      description: Opaque continuation cursor returned by the preceding page.
      schema:
        $ref: "#/components/schemas/Cursor"
    PageLimit:
      name: limit
      in: query
      required: false
      description: Maximum number of resources to return.
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 50
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: true
      description: Opaque client-generated key for safely retrying this operation.
      schema:
        type: string
        minLength: 16
        maxLength: 200
    IfMatch:
      name: If-Match
      in: header
      required: true
      description: Entity tag containing the resource version last observed by the client.
      schema:
        type: string
        pattern: ^"[1-9][0-9]*"$
    PolicyIfMatch:
      name: If-Match
      in: header
      required: true
      description: Entity tag containing the last policy revision; `"0"` creates the first persisted policy.
      schema:
        type: string
        pattern: ^"(?:0|[1-9][0-9]*)"$
    CSRFToken:
      name: X-CSRF-Token
      in: header
      required: true
      description: |
        Session-bound CSRF token. It must exactly match the
        `__Host-assign_csrf` cookie sent with the request.
      schema:
        type: string
        minLength: 43
        maxLength: 43
        pattern: ^[A-Za-z0-9_-]{43}$
    Provider:
      name: provider
      in: path
      required: true
      description: Identity provider name.
      schema:
        type: string
        enum:
          - google
          - github
          - apple
    WorkspaceId:
      name: workspace_id
      in: path
      required: true
      schema:
        $ref: "#/components/schemas/Identifier"
    LifecycleDefinitionId:
      name: definition_id
      in: path
      required: true
      schema:
        $ref: "#/components/schemas/Identifier"
    LifecycleMigrationJobId:
      name: job_id
      in: path
      required: true
      schema:
        $ref: "#/components/schemas/Identifier"
    PersonUsername:
      name: username
      in: path
      required: true
      description: Current or historical public username. UUIDv7 is accepted only for migration from pre-username profile links.
      schema:
        $ref: "#/components/schemas/PersonReference"
    ProjectId:
      name: project_id
      in: path
      required: true
      schema:
        $ref: "#/components/schemas/Identifier"
    ProjectStateId:
      name: state_id
      in: path
      required: true
      schema:
        $ref: "#/components/schemas/Identifier"
    ProjectCode:
      name: project_code
      in: path
      required: true
      description: Case-insensitive immutable Project key, normalized to uppercase before lookup.
      schema:
        $ref: "#/components/schemas/ProjectKey"
    StatusId:
      name: status_id
      in: path
      required: true
      schema:
        $ref: "#/components/schemas/Identifier"
    TaskId:
      name: task_id
      in: path
      required: true
      schema:
        $ref: "#/components/schemas/Identifier"
    TaskCode:
      name: task_code
      in: path
      required: true
      description: Case-insensitive UI-equivalent Task code (`PROJECT_KEY-TASK_NUMBER`, for example `PRO-123`), normalized to uppercase before lookup.
      schema:
        $ref: "#/components/schemas/TaskCode"
    CommentId:
      name: comment_id
      in: path
      required: true
      schema:
        $ref: "#/components/schemas/Identifier"
    CommentReaction:
      name: reaction
      in: path
      required: true
      schema:
        $ref: "#/components/schemas/CommentReactionKey"
    DocumentId:
      name: document_id
      in: path
      required: true
      schema:
        $ref: "#/components/schemas/Identifier"
    AttachmentId:
      name: attachment_id
      in: path
      required: true
      schema:
        $ref: "#/components/schemas/Identifier"
    UploadId:
      name: upload_id
      in: path
      required: true
      schema:
        $ref: "#/components/schemas/Identifier"
    SessionId:
      name: session_id
      in: path
      required: true
      schema:
        $ref: "#/components/schemas/Identifier"
    IdentityId:
      name: identity_id
      in: path
      required: true
      schema:
        $ref: "#/components/schemas/Identifier"
    MemberId:
      name: member_id
      in: path
      required: true
      schema:
        $ref: "#/components/schemas/Identifier"
    InvitationId:
      name: invitation_id
      in: path
      required: true
      schema:
        $ref: "#/components/schemas/Identifier"
  headers:
    ProjectTimeReportRevision:
      description: Workspace report snapshot revision. Compare across every page and all four Project time reads in one selected range; restart the entire range after a mismatch. The value may advance for unrelated Workspace writes.
      schema:
        type: string
        pattern: ^([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}|none):(0|[1-9][0-9]*)$
    RealtimeReadCursor:
      description: Signed Workspace/User/session-bound replay custody; present only after a successful requested canonical baseline.
      schema:
        type: string
        maxLength: 4096
    RealtimeReadPosition:
      description: Decimal-string snapshot position; present only with a successful baseline cursor.
      schema:
        type: string
        pattern: ^(0|[1-9][0-9]*)$
    RequestId:
      description: Correlation identifier for the request.
      schema:
        type: string
        minLength: 1
        maxLength: 200
    CacheControlNoStore:
      description: Prevents storage of error responses by clients and intermediaries.
      schema:
        type: string
        const: no-store
    ContentTypeOptions:
      description: Prevents user agents from MIME-sniffing the response.
      schema:
        type: string
        const: nosniff
    ETag:
      description: Quoted decimal optimistic-concurrency version.
      schema:
        type: string
        pattern: ^"[1-9][0-9]*"$
  schemas:
    HomeWorkDisplayLabel:
      type: object
      additionalProperties: false
      required:
        - id
        - name
        - color
      properties:
        id:
          $ref: "#/components/schemas/Identifier"
        name:
          type: string
        color:
          $ref: "#/components/schemas/PaletteColor"
    HomeWorkTask:
      type: object
      additionalProperties: false
      required:
        - id
        - workspace_id
        - project_id
        - task_number
        - code
        - status_id
        - rank
        - title
        - priority
        - revision
        - created_at
        - updated_at
        - archived_at
        - trashed_at
        - purge_after
        - purged_at
        - lifecycle
        - resolved_at
        - resolved_by
        - resolution
        - summary
        - display_labels
      properties:
        status_lifecycle:
          $ref: "#/components/schemas/ResolvedLifecycle"
        id:
          $ref: "#/components/schemas/Identifier"
        workspace_id:
          $ref: "#/components/schemas/Identifier"
        project_id:
          $ref: "#/components/schemas/Identifier"
        task_number:
          type: integer
          minimum: 1
        code:
          $ref: "#/components/schemas/TaskCode"
        status_id:
          $ref: "#/components/schemas/Identifier"
        rank:
          type: string
          minLength: 1
          description: Server-owned sortable order key within the Task's Project and Status. Clients render it but submit neighbour anchors when moving a Task.
        assignee_actor_id:
          oneOf:
            - $ref: "#/components/schemas/Identifier"
            - type: "null"
        title:
          type: string
          minLength: 1
          maxLength: 500
        priority:
          $ref: "#/components/schemas/TaskPriority"
        description:
          oneOf:
            - $ref: "#/components/schemas/RichTextDocument"
            - type: "null"
        summary:
          description: Revision-bound derived convenience text. It is null when the body is shorter than 300 normalized characters, generation is pending, or the stored projection is stale. It is never evidence or a replacement for description content.
          oneOf:
            - $ref: "#/components/schemas/GeneratedResourceSummary"
            - type: "null"
        due_on:
          oneOf:
            - type: string
              format: date
            - type: "null"
        milestone_id:
          oneOf:
            - $ref: "#/components/schemas/Identifier"
            - type: "null"
        completed_at:
          description: Server-written instant when the Task last entered a done-category Status; null while open or unavailable for a pre-migration record.
          oneOf:
            - $ref: "#/components/schemas/Timestamp"
            - type: "null"
        resolved_at:
          description: Instant the current terminal episode began; cleared when the Task becomes active again.
          oneOf:
            - $ref: "#/components/schemas/Timestamp"
            - type: "null"
        resolved_by:
          description: Actor that started the current terminal episode; null only for migrated or trusted automated provenance gaps.
          oneOf:
            - $ref: "#/components/schemas/Identifier"
            - type: "null"
        resolution:
          description: Terminal outcome for the current resolved episode; null for active Tasks.
          oneOf:
            - $ref: "#/components/schemas/TaskResolution"
            - type: "null"
        label_ids:
          description: Task collections include the complete assignment identifier set; an empty array means no labels. Non-collection responses may omit this field; use the target-label read when absent.
          type: array
          maxItems: 20
          items:
            $ref: "#/components/schemas/Identifier"
        reversible_command:
          description: Present only on a mutation response when the server created an undoable command. Read responses omit it.
          $ref: "#/components/schemas/ReversibleCommand"
        revision:
          type: integer
          minimum: 1
        created_at:
          $ref: "#/components/schemas/Timestamp"
        updated_at:
          $ref: "#/components/schemas/Timestamp"
        archived_at:
          oneOf:
            - $ref: "#/components/schemas/Timestamp"
            - type: "null"
        trashed_at:
          oneOf:
            - $ref: "#/components/schemas/Timestamp"
            - type: "null"
        purge_after:
          description: Fixed retention deadline after which the asynchronous purge sweeper may redact Task content; null outside trash and after purge.
          oneOf:
            - $ref: "#/components/schemas/Timestamp"
            - type: "null"
        purged_at:
          description: Tombstone creation instant. A purged Task retains only stable reference metadata and cannot be restored.
          oneOf:
            - $ref: "#/components/schemas/Timestamp"
            - type: "null"
        lifecycle:
          type: string
          enum:
            - active
            - archived
            - trashed
            - purged
        display_labels:
          type: array
          maxItems: 1000
          items:
            $ref: "#/components/schemas/HomeWorkDisplayLabel"
      description: Task in an opt-in Home/Work baseline. Ordinary Task responses are unchanged.
    HomeWorkPage:
      type: object
      additionalProperties: false
      required:
        - items
        - has_more
        - as_of_date
        - timezone
      properties:
        total_count:
          type: integer
          format: int64
          minimum: 0
          description: Authorized collection size before cursor bounds; refreshed at read time. Activity counts admitted events before display grouping and Comments include retained ordinals.
        items:
          type: array
          maxItems: 100
          items:
            $ref: "#/components/schemas/HomeWorkTask"
        next_cursor:
          oneOf:
            - $ref: "#/components/schemas/Cursor"
            - type: "null"
        has_more:
          type: boolean
        as_of_date:
          type: string
          format: date
        timezone:
          type: string
          description: Persisted IANA timezone used to classify date-only due dates.
    HomeWorkFollowingPage:
      type: object
      additionalProperties: false
      required:
        - items
        - has_more
      properties:
        items:
          type: array
          maxItems: 100
          items:
            $ref: "#/components/schemas/HomeWorkTask"
        next_cursor:
          oneOf:
            - $ref: "#/components/schemas/Cursor"
            - type: "null"
        has_more:
          type: boolean
    HomeWorkProjects:
      type: object
      additionalProperties: false
      required:
        - items
        - has_more
        - next_cursor
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/Project"
          maxItems: 100
        next_cursor:
          oneOf:
            - $ref: "#/components/schemas/Cursor"
            - type: "null"
          const: null
        has_more:
          type: boolean
          const: false
    HomeWorkStatuses:
      type: object
      additionalProperties: false
      required:
        - items
        - has_more
        - next_cursor
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/Status"
          maxItems: 100
        next_cursor:
          oneOf:
            - $ref: "#/components/schemas/Cursor"
            - type: "null"
          const: null
        has_more:
          type: boolean
          const: false
    HomeWorkWorkspaceChoices:
      type: object
      additionalProperties: false
      required:
        - items
        - has_more
        - next_cursor
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/WorkspaceSummary"
          maxItems: 100
        next_cursor:
          oneOf:
            - $ref: "#/components/schemas/Cursor"
            - type: "null"
          const: null
        has_more:
          type: boolean
          const: false
    HomeWorkWorkspaceBaseline:
      type: object
      additionalProperties: false
      required:
        - version
        - workspace_id
        - cursor
        - scope
        - view
        - work
        - following
        - projects
        - statuses
      properties:
        version: &a20
          type: integer
          enum:
            - 1
        workspace_id: &a21
          $ref: "#/components/schemas/Identifier"
        cursor: &a22
          type: string
          minLength: 1
          maxLength: 4096
          description: Opaque signed realtime cursor for this scope; distinct from Work pagination.
        scope:
          type: string
          enum:
            - workspace
        view:
          $ref: "#/components/schemas/MyWorkView"
        work:
          $ref: "#/components/schemas/HomeWorkPage"
        following:
          oneOf:
            - $ref: "#/components/schemas/HomeWorkFollowingPage"
            - type: "null"
        projects:
          $ref: "#/components/schemas/HomeWorkProjects"
        statuses:
          $ref: "#/components/schemas/HomeWorkStatuses"
    HomeWorkAccountBaseline:
      type: object
      additionalProperties: false
      required:
        - version
        - workspace_id
        - cursor
        - scope
        - me
        - workspaces
        - current_work
        - current_task
      properties:
        version: *a20
        workspace_id: *a21
        cursor: *a22
        scope:
          type: string
          enum:
            - account
        me:
          $ref: "#/components/schemas/Me"
        workspaces:
          $ref: "#/components/schemas/HomeWorkWorkspaceChoices"
        current_work:
          $ref: "#/components/schemas/CurrentWorkState"
        current_task:
          oneOf:
            - $ref: "#/components/schemas/HomeWorkTask"
            - type: "null"
    RealtimeBaseline:
      type: object
      required:
        - version
        - scope
        - cursor
      properties:
        version:
          type: integer
          enum:
            - 1
        scope:
          type: string
          enum:
            - workspace
            - projects
            - task_comments
            - account
            - account_security
            - inbox
            - personal_projects
            - discuss
        cursor:
          type: string
          minLength: 1
          maxLength: 4096
        resource_id:
          type: string
          description: Task UUID for task_comments; empty for projects; absent for routing custody.
        through:
          type: string
          pattern: ^(0|[1-9][0-9]*)$
          description: Decimal-string page snapshot position; absent for routing-only baselines.
        next_page:
          type: string
          description: Opaque domain continuation; empty on the final page.
        items:
          type: array
          maxItems: 100
          description: Present only for projects or task_comments; canonical resource projections.
          items:
            oneOf:
              - $ref: "#/components/schemas/Project"
              - $ref: "#/components/schemas/Comment"
    EvidenceCapture:
      type: object
      properties:
        entity_type:
          type: string
          enum:
            - task
            - document
            - project
            - comment
        entity_id:
          type: string
          format: uuid
        revision:
          type: integer
          format: int64
          minimum: 1
        field:
          type: string
          enum:
            - title
            - name
            - description
            - content
            - body
        quote:
          type: string
          minLength: 1
          maxLength: 1800
      required:
        - entity_type
        - entity_id
        - revision
        - field
        - quote
      additionalProperties: false
      description: One unique exact substring in server-extracted plain text. Task title/description, Document title/content, Project name/description and Comment body only. Ambiguous, missing, archived or stale sources fail; quote is not persisted.
    EvidenceCollectionCreateRequest:
      type: object
      properties:
        sources:
          type: array
          items:
            $ref: "#/components/schemas/EvidenceCapture"
          minItems: 1
          maxItems: 32
      required:
        - sources
      additionalProperties: false
    EvidenceCollectionExcludeRequest:
      type: object
      properties:
        id:
          type: string
          format: uuid
        expected_version:
          type: integer
          format: int64
          minimum: 1
        excluded_ids:
          type: array
          items:
            type: string
            format: uuid
          minItems: 0
          maxItems: 32
      required:
        - id
        - expected_version
        - excluded_ids
      additionalProperties: false
      description: Complete replacement exclusion set for this investigation. Empty array includes all original sources again. Does not change other collections or enduring memory.
    EvidenceReference:
      type: object
      properties:
        id:
          type: string
          format: uuid
        entity_type:
          type: string
        entity_id:
          type: string
          format: uuid
        revision:
          type: integer
          format: int64
          minimum: 1
        field:
          type: string
        project_id:
          type:
            - string
            - "null"
          format: uuid
        start:
          type: integer
          minimum: 0
        end:
          type: integer
          minimum: 1
        fingerprint:
          type: string
          pattern: ^[a-f0-9]{64}$
        excluded:
          type: boolean
      required:
        - id
        - entity_type
        - entity_id
        - revision
        - field
        - project_id
        - start
        - end
        - fingerprint
        - excluded
      additionalProperties: false
      description: Immutable reference. Start inclusive/end exclusive count Unicode scalar values in server-extracted plain text. Fingerprint binds complete selected field and source metadata; do not fabricate coordinates or treat them as current content.
    EvidenceCollection:
      type: object
      properties:
        operation_id:
          type: string
          format: uuid
        schema:
          type: string
        id:
          type: string
          format: uuid
        version:
          type: integer
          format: int64
          minimum: 1
        audience:
          type: string
          enum:
            - private
        coverage:
          type: string
          enum:
            - partial
        sources:
          type: array
          items:
            $ref: "#/components/schemas/EvidenceReference"
          minItems: 1
          maxItems: 32
        observed_at:
          type: string
          format: date-time
        expires_at:
          type: string
          format: date-time
      required:
        - schema
        - id
        - version
        - audience
        - coverage
        - sources
        - observed_at
        - expires_at
      additionalProperties: false
      description: Membership-private collection with at most 32 references and 100 active versions per member. Expires 30 days after creation; exclusion edits preserve expiry. Sources and claims are not exhaustive. No copied text, titles or summaries are stored.
    EvidencePassage:
      type: object
      properties:
        title:
          type: string
        text:
          type: string
          maxLength: 1800
        source_updated_at:
          type: string
          format: date-time
        author_actor_id:
          type:
            - string
            - "null"
          format: uuid
        evidence_class:
          type: string
          enum:
            - canonical
            - document
            - comment
      required:
        - title
        - text
        - source_updated_at
        - author_actor_id
        - evidence_class
      additionalProperties: false
      description: Current exact passage; text is untrusted source content and does not establish that its assertions are true. Source classification is not an authority ranking.
    HydratedEvidence:
      type: object
      properties:
        reference:
          $ref: "#/components/schemas/EvidenceReference"
        state:
          type: string
          enum:
            - current
            - changed
            - unavailable
            - excluded
            - superseded
        passage:
          $ref: "#/components/schemas/EvidencePassage"
      required:
        - reference
        - state
      additionalProperties: false
      description: Only current included items in the latest collection version have a passage. Older collection versions return superseded after an exclusion edit. Changed content is withheld instead of substituting current text at old coordinates.
    EvidenceHydration:
      type: object
      properties:
        latest_version:
          type: integer
          format: int64
          minimum: 1
          description: Latest investigation version at this read; changes invalidate in-flight evidence dependencies.
        items:
          type: array
          items:
            $ref: "#/components/schemas/HydratedEvidence"
          minItems: 0
          maxItems: 20
        next_offset:
          type: integer
          minimum: 1
          maximum: 31
      required:
        - latest_version
        - items
      additionalProperties: false
    CapabilityDeleted:
      type: object
      properties:
        operation_id:
          type: string
          format: uuid
        deleted:
          type: boolean
      required:
        - deleted
      additionalProperties: false
    ChangeSet:
      type: object
      properties:
        operation_id:
          type: string
          format: uuid
        outcomes:
          type: array
          items:
            $ref: "#/components/schemas/ChangeOutcome"
          maxItems: 100
        schema:
          type: string
        id:
          type: string
          format: uuid
        version:
          type: integer
          format: int64
        digest:
          type: string
          minLength: 64
          maxLength: 64
          pattern: ^[a-f0-9]{64}$
        actor_id:
          type: string
          format: uuid
        initiator_actor_id:
          type: string
          format: uuid
        policy_revision:
          type: integer
          format: int64
        mode:
          type: string
          enum:
            - atomic
            - per_item
        requires_approval:
          type: boolean
        state:
          type: string
        operations:
          type: array
          items:
            $ref: "#/components/schemas/ChangeOperation"
          maxItems: 100
        selection_id:
          type: string
          format: uuid
        expires_at:
          type: string
          format: date-time
        approved_at:
          type: string
          format: date-time
        review_url:
          type: string
      required:
        - schema
        - id
        - version
        - digest
        - actor_id
        - initiator_actor_id
        - policy_revision
        - mode
        - requires_approval
        - state
        - operations
        - expires_at
        - review_url
      additionalProperties: false
      description: Immutable versioned proposal. Exact digest and initiating-human approval are required for compound changes, creates and Document applies. Approval expires after24 hours; proposal after30 days. Editing invalidates approval. Review URL identifies the same authorized HTTP resource; it grants no authority.
    ChangeSetApplyRequest:
      type: object
      properties:
        id:
          type: string
          format: uuid
        version:
          type: integer
          format: int64
        digest:
          type: string
          minLength: 64
          maxLength: 64
          pattern: ^[a-f0-9]{64}$
      required:
        - id
        - version
        - digest
      additionalProperties: false
    ChangeSetCreateRequest:
      type: object
      properties:
        operations:
          type: array
          items:
            $ref: "#/components/schemas/ChangeOperation"
          maxItems: 100
          minItems: 1
        mode:
          type: string
          enum:
            - atomic
            - per_item
        selection_id:
          type: string
          format: uuid
        id:
          type: string
          format: uuid
        expected_version:
          type: integer
          format: int64
      required:
        - operations
        - mode
      additionalProperties: false
    ChangeSetResult:
      type: object
      properties:
        operation_id:
          type: string
          format: uuid
        id:
          type: string
          format: uuid
        version:
          type: integer
          format: int64
        state:
          type: string
        outcomes:
          type: array
          items:
            $ref: "#/components/schemas/ChangeOutcome"
          maxItems: 100
      required:
        - id
        - version
        - state
        - outcomes
      additionalProperties: false
    ChangeSetReviewRequest:
      type: object
      properties:
        version:
          type: integer
          format: int64
        digest:
          type: string
          minLength: 64
          maxLength: 64
          pattern: ^[a-f0-9]{64}$
        decision:
          type: string
          enum:
            - approve
            - reject
      required:
        - version
        - digest
        - decision
      additionalProperties: false
    DocumentPatchCreateRequest:
      type: object
      properties:
        document_id:
          type: string
          format: uuid
        base_revision:
          type: integer
          format: int64
        content:
          type: object
          additionalProperties: true
        id:
          type: string
          format: uuid
        expected_version:
          type: integer
          format: int64
      required:
        - document_id
        - base_revision
        - content
      additionalProperties: false
    DocumentPatchDraft:
      type: object
      properties:
        operation_id:
          type: string
          format: uuid
        schema:
          type: string
        id:
          type: string
          format: uuid
        version:
          type: integer
          format: int64
        document_id:
          type: string
          format: uuid
        base_revision:
          type: integer
          format: int64
        base_content:
          type: object
          additionalProperties: true
        draft_content:
          type: object
          additionalProperties: true
        actor_id:
          type: string
          format: uuid
        applied_revision:
          type: integer
          format: int64
        expires_at:
          type: string
          format: date-time
      required:
        - schema
        - id
        - version
        - document_id
        - base_revision
        - base_content
        - draft_content
        - actor_id
        - expires_at
      additionalProperties: false
      description: Private immutable draft version with exact base/current apply revision. Does not mutate the Document until included in a reviewed ChangeSet. Expires after30 days.
    IdentityReference:
      type: object
      properties:
        id:
          type: string
          format: uuid
      required:
        - id
      additionalProperties: false
    MemberResolution:
      type: object
      properties:
        match:
          type: string
          enum:
            - exact
            - ambiguous
            - missing
        candidates:
          type: array
          items:
            $ref: "#/components/schemas/MemberCandidate"
          maxItems: 20
        truncated:
          type: boolean
      required:
        - match
        - candidates
        - truncated
      additionalProperties: false
    OperationReceipt:
      type: object
      properties:
        schema:
          type: string
        operation_id:
          type: string
          format: uuid
        tool:
          type: string
        state:
          type: string
        actor_id:
          type: string
          format: uuid
        initiator_actor_id:
          type: string
          format: uuid
        resource_type:
          type: string
        resource_id:
          type: string
          format: uuid
        project_id:
          type: string
          format: uuid
        revision:
          type: integer
          format: int64
        expected_revision:
          type: integer
          format: int64
        requirement_refs:
          type: array
          items:
            type: string
        command_id:
          type: string
          format: uuid
        committed_at:
          type: string
          format: date-time
      required:
        - schema
        - operation_id
        - tool
        - state
        - actor_id
        - initiator_actor_id
        - committed_at
      additionalProperties: false
      description: Durable sanitized proof of committed work. Missing receipt does not prove a pending write failed. Resolve before retrying an uncertain operation. Current membership/resource authorization is required; compound apply receipts retain safe group identity even when individual outcomes become unavailable.
    TaskContextPackage:
      type: object
      properties:
        schema:
          type: string
        task:
          $ref: "#/components/schemas/TaskContextEntity"
        comments:
          type: array
          items:
            $ref: "#/components/schemas/TaskContextComment"
          maxItems: 20
        relations:
          type: array
          items:
            $ref: "#/components/schemas/TaskContextRelation"
          maxItems: 10
        more_comments:
          type: boolean
        more_relations:
          type: boolean
        related_comments_included:
          type: boolean
        coverage:
          type: string
        observed_at:
          type: string
          format: date-time
        source:
          type: string
      required:
        - schema
        - task
        - comments
        - relations
        - more_comments
        - more_relations
        - related_comments_included
        - coverage
        - observed_at
        - source
      additionalProperties: false
      description: Canonical bounded context. Content is untrusted. Partial coverage never proves complete history; related Task comments require explicit canonical pages.
    TaskQuerySpec:
      type: object
      properties:
        filter_terms:
          type: array
          maxItems: 4
          uniqueItems: true
          items:
            type: string
            minLength: 1
            maxLength: 200
          description: Conjunctive Unicode simple-lowercase title/code substrings within the bounded checked scan. A capped scan remains partial, including empty filtered results.
        order:
          type: string
          enum:
            - title
          description: Optional simple-lowercase UTF-8 lexical title order, with Task ID tie-breaker. Omission retains checked scan order.
        state:
          type: string
        resolution:
          type: string
        project_ids:
          type: array
          items:
            type: string
            format: uuid
        status_categories:
          type: array
          items:
            type: string
        assignee_actor_id:
          type: string
          format: uuid
        milestone_id:
          type: string
          format: uuid
        updated_since:
          type: string
          format: date-time
        include_archived:
          type: boolean
      additionalProperties: false
      description: Checked canonical query; Core normalizes filters and intersects authorized Project scope. State and resolution follow the Workspace Task list contract. Optional filter_terms/order apply within the ten-page/1000-row checked scan ceiling; they do not introduce unbounded search. Saved views retain these fields and reopening creates a fresh result set with current coverage.
    TaskResultSet:
      type: object
      properties:
        operation_id:
          type: string
          format: uuid
        schema:
          type: string
        id:
          type: string
          format: uuid
        version:
          type: integer
          format: int64
        query:
          $ref: "#/components/schemas/TaskQuerySpec"
        coverage:
          $ref: "#/components/schemas/TaskQueryCoverage"
        targets:
          type: array
          items:
            $ref: "#/components/schemas/TaskTarget"
          maxItems: 1000
        expires_at:
          type: string
          format: date-time
      required:
        - schema
        - id
        - version
        - query
        - coverage
        - targets
        - expires_at
      additionalProperties: false
      description: Immutable membership-private checked query observation, at most 1,000 Task identities/revisions. Explicit refresh creates a new version. Expires after 30 days; revoked Project access makes its snapshot unavailable.
    TaskResultSetCreateRequest:
      type: object
      properties:
        query:
          $ref: "#/components/schemas/TaskQuerySpec"
        refresh_id:
          type: string
          format: uuid
        expected_version:
          type: integer
          format: int64
      required:
        - query
      additionalProperties: false
    ProjectTaskViewDefinition:
      type: object
      description: Membership-private Project Task presentation. Maximum serialized size is 16 KiB. Query Project scope must match project_id. Missing parameters use the current Project Task presentation defaults; unsupported keys or values are rejected. An update omitting definition preserves the existing definition, and an existing definition cannot move to another Project.
      properties:
        project_id:
          type: string
          format: uuid
        layout:
          type: string
          enum:
            - list
            - board
            - backlog
        parameters:
          type: object
          maxProperties: 13
          propertyNames:
            enum:
              - label
              - milestone
              - status
              - assignee
              - priority
              - due
              - created
              - updated
              - group
              - sort
              - completed
              - done
              - labels
          additionalProperties:
            type: array
            maxItems: 100
            items:
              type: string
          description: Existing Project Task URL parameters. Label, milestone and status accept UUIDs; assignee accepts UUIDs or unassigned. Label selections match any selected assigned Task label and compose with the other filters on the bounded loaded Project collection; label filtering does not change server pagination or aggregate totals. Priority accepts none, urgent, high, medium or low. Due accepts overdue, today, next7, next30 or none; created and updated accept today, last7 or last30. These date parameters also accept an inclusive YYYY-MM-DD..YYYY-MM-DD range with either endpoint optionally omitted. Group accepts none, status, assignee or milestone; sort accepts manual, newest, oldest or priority; completed accepts recent, week, month or all; done and labels accept shown or hidden. Singleton parameters reject multiple values.
        show_cancelled:
          type: boolean
      required:
        - project_id
        - layout
        - parameters
        - show_cancelled
      additionalProperties: false
    TaskSavedView:
      type: object
      properties:
        operation_id:
          type: string
          format: uuid
        schema:
          type: string
        id:
          type: string
          format: uuid
        revision:
          type: integer
          format: int64
        name:
          type: string
          minLength: 1
          maxLength: 100
        query:
          $ref: "#/components/schemas/TaskQuerySpec"
        definition:
          $ref: "#/components/schemas/ProjectTaskViewDefinition"
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
      required:
        - schema
        - id
        - revision
        - name
        - query
        - created_at
        - updated_at
      additionalProperties: false
      description: Membership-private named query. Opening runs a new result-set query. Requires an unrestricted Workspace credential; Project-restricted credentials cannot list or modify personal saved views.
    TaskSelection:
      type: object
      properties:
        operation_id:
          type: string
          format: uuid
        id:
          type: string
          format: uuid
        result_set_id:
          type: string
          format: uuid
        result_set_version:
          type: integer
          format: int64
        task_ids:
          type: array
          items:
            type: string
            format: uuid
          minItems: 1
          maxItems: 1000
          uniqueItems: true
      required:
        - id
        - result_set_id
        - result_set_version
        - task_ids
      additionalProperties: false
    TaskSelectionCreateRequest:
      type: object
      properties:
        result_set_id:
          type: string
          format: uuid
        result_set_version:
          type: integer
          format: int64
        task_ids:
          type: array
          items:
            type: string
            format: uuid
          minItems: 1
          maxItems: 1000
          uniqueItems: true
      required:
        - result_set_id
        - result_set_version
        - task_ids
      additionalProperties: false
    TaskSelectionHydration:
      type: object
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/HydratedTaskTarget"
          maxItems: 100
        next_offset:
          type: integer
          format: int64
      required:
        - items
      additionalProperties: false
    TaskViewDeleteRequest:
      type: object
      properties:
        id:
          type: string
          format: uuid
        expected_revision:
          type: integer
          format: int64
      required:
        - id
        - expected_revision
      additionalProperties: false
    TaskViewList:
      type: object
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/TaskSavedView"
          maxItems: 100
      required:
        - items
      additionalProperties: false
    TaskViewSaveRequest:
      type: object
      properties:
        id:
          type: string
          format: uuid
        expected_revision:
          type: integer
          format: int64
        name:
          type: string
          minLength: 1
          maxLength: 100
        query:
          $ref: "#/components/schemas/TaskQuerySpec"
        definition:
          $ref: "#/components/schemas/ProjectTaskViewDefinition"
      required:
        - name
        - query
      additionalProperties: false
    ChangeOperation:
      type: object
      properties:
        operation_id:
          type: string
          format: uuid
          description: Core-assigned immutable operation identity. Omit when proposing; client-provided values are rejected.
          readOnly: true
        tool:
          type: string
          enum:
            - task_update
            - task_complete
            - task_create
            - document_patch_apply
        task_id:
          type: string
          format: uuid
        project_id:
          type: string
          format: uuid
        expected_revision:
          type: integer
          format: int64
        status_id:
          type: string
          format: uuid
        status_revision:
          type: integer
          format: int64
        title:
          type: string
        priority:
          type: string
        assignee_actor_id:
          type: string
          format: uuid
        description:
          type: object
          additionalProperties: true
        draft_id:
          type: string
          format: uuid
        draft_version:
          type: integer
          format: int64
        requirement_refs:
          type: array
          items:
            type: string
            maxLength: 256
          maxItems: 32
          description: At most 2,048 bytes for the JSON-serialized array in addition to per-item limits.
        excluded:
          type: boolean
      required:
        - tool
        - excluded
      additionalProperties: false
    ChangeOutcome:
      type: object
      properties:
        operation_id:
          type: string
          format: uuid
        state:
          type: string
          enum:
            - committed
            - conflicted
            - denied
            - failed
            - excluded
            - unavailable
        receipt:
          $ref: "#/components/schemas/OperationReceipt"
      required:
        - operation_id
        - state
      additionalProperties: false
    TaskTarget:
      type: object
      properties:
        task_id:
          type: string
          format: uuid
        project_id:
          type: string
          format: uuid
        observed_revision:
          type: integer
          format: int64
      required:
        - task_id
        - project_id
        - observed_revision
      additionalProperties: false
    TaskQueryCoverage:
      type: object
      properties:
        query_digest:
          type: string
        collection_version:
          type: string
        visibility_revision:
          type: string
        observed_at:
          type: string
          format: date-time
        returned_count:
          type: integer
          format: int64
        coverage:
          type: string
        limit_reason:
          type: string
      required:
        - query_digest
        - collection_version
        - visibility_revision
        - observed_at
        - returned_count
        - coverage
      additionalProperties: false
    HydratedTaskTarget:
      type: object
      properties:
        task_id:
          type: string
          format: uuid
        project_id:
          type: string
          format: uuid
        observed_revision:
          type: integer
          format: int64
        state:
          type: string
          enum:
            - current
            - changed
            - unavailable
        preview:
          $ref: "#/components/schemas/TaskResultPreview"
        current_revision:
          type: integer
          format: int64
      required:
        - task_id
        - project_id
        - observed_revision
        - state
      additionalProperties: false
    TaskResultPreview:
      type: object
      description: Current authorized compact Task fields; omitted for unavailable targets. Never replaces snapshot membership or observed revision. Contains no Task body.
      properties:
        id:
          type: string
          format: uuid
        project_id:
          type: string
          format: uuid
        task_number:
          type: integer
          format: int64
        task_code:
          type: string
        title:
          type: string
        status_id:
          type: string
          format: uuid
        assignee_actor_id:
          type:
            - string
            - "null"
          format: uuid
        lifecycle:
          type: string
      required:
        - id
        - project_id
        - task_number
        - task_code
        - title
        - status_id
        - assignee_actor_id
        - lifecycle
      additionalProperties: false
    MemberCandidate:
      type: object
      properties:
        membership_id:
          type: string
          format: uuid
        actor_id:
          type: string
          format: uuid
        display_name:
          type: string
        username:
          type: string
        revision:
          type: integer
          format: int64
      required:
        - membership_id
        - actor_id
        - display_name
        - username
        - revision
      additionalProperties: false
    TaskContextEntity:
      type: object
      properties:
        id:
          type: string
          format: uuid
        revision:
          type: integer
          format: int64
        code:
          type: string
        title:
          type: string
        description_text:
          type: string
        truncated:
          type: boolean
      required:
        - id
        - revision
        - code
        - title
        - description_text
        - truncated
      additionalProperties: false
    TaskContextComment:
      type: object
      properties:
        id:
          type: string
          format: uuid
        revision:
          type: integer
          format: int64
        text:
          type: string
        truncated:
          type: boolean
        deleted:
          type: boolean
      required:
        - id
        - revision
        - text
        - truncated
        - deleted
      additionalProperties: false
    TaskContextRelation:
      type: object
      properties:
        id:
          type: string
          format: uuid
        type:
          type: string
        source_task_id:
          type: string
          format: uuid
        target_task_id:
          type: string
          format: uuid
        related_task:
          $ref: "#/components/schemas/TaskContextEntity"
      required:
        - id
        - type
        - source_task_id
        - target_task_id
        - related_task
      additionalProperties: false
    TimeTrackingPolicy:
      type: object
      required:
        - workspace_id
        - revision
        - enabled
        - rounding_mode
      properties:
        workspace_id:
          $ref: "#/components/schemas/Identifier"
        revision:
          type: integer
          minimum: 0
        enabled:
          type: boolean
        rounding_mode:
          type: string
          enum:
            - exact
            - ceil_15_minutes
            - ceil_30_minutes
        updated_at:
          $ref: "#/components/schemas/Timestamp"
    TimeTrackingPolicyUpdateRequest:
      type: object
      required:
        - enabled
        - rounding_mode
      properties:
        enabled:
          type: boolean
        rounding_mode:
          type: string
          enum:
            - exact
            - ceil_15_minutes
            - ceil_30_minutes
    TimeEntry:
      type: object
      required:
        - id
        - workspace_id
        - project_id
        - task_id
        - actor_id
        - work_date
        - exact_duration_seconds
        - reportable_duration_seconds
        - rounding_policy_revision
        - revision
        - created_at
        - updated_at
      properties:
        id:
          $ref: "#/components/schemas/Identifier"
        workspace_id:
          $ref: "#/components/schemas/Identifier"
        project_id:
          $ref: "#/components/schemas/Identifier"
        task_id:
          $ref: "#/components/schemas/Identifier"
        actor_id:
          $ref: "#/components/schemas/Identifier"
        work_date:
          type: string
          format: date
        exact_duration_seconds:
          type: integer
          minimum: 1
        reportable_duration_seconds:
          type: integer
          minimum: 1
        rounding_policy_revision:
          type: integer
          minimum: 1
        duration_input:
          type: string
          maxLength: 128
        note:
          type: string
          maxLength: 2000
        revision:
          type: integer
          minimum: 1
        created_at:
          $ref: "#/components/schemas/Timestamp"
        updated_at:
          $ref: "#/components/schemas/Timestamp"
    TimeEntryCreateRequest:
      type: object
      required:
        - duration
        - work_date
      properties:
        duration:
          type: string
          minLength: 2
          maxLength: 128
        work_date:
          type: string
          format: date
        note:
          type: string
          maxLength: 2000
    TimeEntryUpdateRequest:
      $ref: "#/components/schemas/TimeEntryCreateRequest"
    TimeEntryList:
      type: object
      required:
        - items
      properties:
        items:
          type: array
          maxItems: 100
          items:
            $ref: "#/components/schemas/TimeEntry"
        next:
          type: string
          description: Signed continuation bound to the Task and report revision; omitted on the final page.
    TimeTotals:
      type: object
      required:
        - exact_duration_seconds
        - reportable_duration_seconds
        - entry_count
      properties:
        exact_duration_seconds:
          type: integer
          minimum: 0
        reportable_duration_seconds:
          type: integer
          minimum: 0
        entry_count:
          type: integer
          minimum: 0
    ProjectTimeTaskBreakdown:
      type: object
      required:
        - items
      properties:
        items:
          type: array
          maxItems: 100
          items:
            $ref: "#/components/schemas/ProjectTimeTaskBreakdownRow"
        next:
          type: string
          description: Signed continuation for the next page; absent on the terminal page.
    ProjectTimeTaskBreakdownRow:
      type: object
      required:
        - task_id
        - task_number
        - task_title
        - exact_duration_seconds
        - reportable_duration_seconds
        - entry_count
      properties:
        task_id:
          type: string
          format: uuid
        task_number:
          type:
            - integer
            - "null"
        task_title:
          type:
            - string
            - "null"
        exact_duration_seconds:
          type: integer
          minimum: 0
        reportable_duration_seconds:
          type: integer
          minimum: 0
        entry_count:
          type: integer
          minimum: 0
    ProjectTimeTaskEntries:
      type: object
      required:
        - items
      properties:
        items:
          type: array
          maxItems: 100
          items:
            $ref: "#/components/schemas/ProjectTimeTaskEntryRow"
        next:
          type: string
          description: Signed continuation for the next page; absent on the terminal page.
    ProjectTimeTaskEntryRow:
      type: object
      required:
        - id
        - task_id
        - actor_id
        - work_date
        - exact_duration_seconds
        - reportable_duration_seconds
        - revision
      properties:
        id:
          type: string
          format: uuid
        task_id:
          type: string
          format: uuid
        actor_id:
          type: string
          format: uuid
        work_date:
          type: string
          format: date
        exact_duration_seconds:
          type: integer
          minimum: 1
        reportable_duration_seconds:
          type: integer
          minimum: 1
        note:
          type: string
          maxLength: 2000
        revision:
          type: integer
          minimum: 1
    ProjectTimeContributorBreakdown:
      type: object
      required:
        - items
        - redacted
      properties:
        items:
          type: array
          maxItems: 100
          items:
            $ref: "#/components/schemas/ProjectTimeContributorBreakdownRow"
        next:
          type: string
          description: Signed continuation for the next page; absent on the terminal page.
        redacted:
          type: boolean
          description: True when the caller may read Project aggregates but not contributor identities.
    ProjectTimeContributorBreakdownRow:
      type: object
      required:
        - actor_id
        - display_name
        - exact_duration_seconds
        - reportable_duration_seconds
        - entry_count
      properties:
        actor_id:
          type: string
          format: uuid
        display_name:
          type:
            - string
            - "null"
        exact_duration_seconds:
          type: integer
          minimum: 0
        reportable_duration_seconds:
          type: integer
          minimum: 0
        entry_count:
          type: integer
          minimum: 0
    TimesheetItem:
      allOf:
        - $ref: "#/components/schemas/TimeEntry"
        - type: object
          required:
            - task_number
            - task_title
            - project_name
          properties:
            task_number:
              type: integer
              minimum: 1
            task_title:
              type: string
            project_name:
              type: string
    Timesheet:
      type: object
      required:
        - items
        - totals
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/TimesheetItem"
        totals:
          $ref: "#/components/schemas/TimeTotals"
        next:
          allOf:
            - $ref: "#/components/schemas/Cursor"
          description: Continuation for the same actor, date range and report revision; absent on the last page.
    Identifier:
      type: string
      format: uuid
      pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$
      description: Opaque Assign-owned UUIDv7 identifier. Clients must not derive meaning from it.
      examples:
        - 0198ac5e-f9b7-7000-8000-000000000001
    AuthorizationRevision:
      type: string
      format: uuid
      description: Opaque Workspace authorization epoch for one actor. This is a revision token, not an Assign-owned entity identifier; clients must compare it only for exact equality and must not derive permissions from it.
      examples:
        - b3503f57-506d-4b5f-aed4-0d231c0c4531
    PublicUsername:
      type: string
      minLength: 3
      maxLength: 30
      pattern: ^[a-z0-9](?:[a-z0-9._-]*[a-z0-9])?$
      description: Globally unique mutable public account handle. New handles use lowercase letters, numbers, and interior dots and are initially derived from the email local part. Existing hyphen/underscore handles remain readable. Previously claimed values remain reserved to the same User.
      examples:
        - jane.doe
    PersonReference:
      type: string
      minLength: 3
      maxLength: 36
      pattern: ^(?:[a-z0-9](?:[a-z0-9._-]{1,28})[a-z0-9]|[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})$
      description: Current or historical public username, or a temporary legacy UUIDv7 profile reference.
    Timestamp:
      type: string
      format: date-time
      description: RFC 3339 timestamp in UTC.
      examples:
        - 2026-08-15T12:00:00Z
    Cursor:
      type: string
      minLength: 1
      maxLength: 2048
      description: Opaque, endpoint-specific pagination cursor.
    Page:
      type: object
      additionalProperties: false
      required:
        - has_more
      properties:
        next_cursor:
          oneOf:
            - $ref: "#/components/schemas/Cursor"
            - type: "null"
        has_more:
          type: boolean
    SearchResourceType:
      type: string
      description: Resource types available from Workspace search. `person` is an active Workspace-membership projection and has no email field.
      enum:
        - project
        - task
        - document
        - comment
        - person
    CliSearchResourceType:
      type: string
      enum:
        - task
        - document
    ResourceCode:
      type: string
      minLength: 1
      maxLength: 64
      pattern: ^[A-Z][A-Z0-9-]{0,63}$
      description: |
        Case-insensitive client input normalized to uppercase by the server.
        A code is unique across Projects, Tasks, and Documents in one
        Workspace. Internal UUIDs are not a CLI identifier.
    TaskCode:
      type: string
      minLength: 4
      maxLength: 28
      pattern: ^[A-Z][A-Z0-9]{1,7}-[1-9][0-9]{0,18}$
      description: |
        Case-insensitive client input normalized to uppercase by the server.
        Exactly the UI ticket reference `{PROJECT_KEY}-{TASK_NUMBER}` (for
        example `PRO-123`); the positive Task number is canonical decimal
        without leading zeroes.
    CliSearchQuery:
      type: string
      minLength: 1
      maxLength: 200
      description: A trimmed Unicode search query containing 1–200 characters.
    CliTaskContextProfile:
      type: string
      default: compact
      enum:
        - compact
        - full
    PersonalApiTokenScope:
      type: string
      enum:
        - assign:read
        - assign:write
        - assign:discuss
    PersonalApiToken:
      type: object
      additionalProperties: false
      required:
        - id
        - prefix
        - name
        - scopes
        - created_at
        - expires_at
      properties:
        id:
          $ref: "#/components/schemas/Identifier"
        prefix:
          type: string
          minLength: 8
          maxLength: 32
        name:
          type: string
          minLength: 1
          maxLength: 100
        scopes:
          type: array
          minItems: 1
          maxItems: 3
          uniqueItems: true
          items:
            $ref: "#/components/schemas/PersonalApiTokenScope"
        created_at:
          $ref: "#/components/schemas/Timestamp"
        last_used_at:
          oneOf:
            - $ref: "#/components/schemas/Timestamp"
            - type: "null"
        expires_at:
          $ref: "#/components/schemas/Timestamp"
    PersonalApiTokenList:
      type: object
      additionalProperties: false
      required:
        - items
        - next_cursor
        - has_more
      properties:
        items:
          type: array
          maxItems: 100
          items:
            $ref: "#/components/schemas/PersonalApiToken"
        next_cursor:
          oneOf:
            - type: string
            - type: "null"
        has_more:
          type: boolean
    PersonalApiTokenCreateRequest:
      type: object
      additionalProperties: false
      required:
        - name
        - scopes
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 100
        scopes:
          type: array
          minItems: 1
          maxItems: 3
          uniqueItems: true
          items:
            $ref: "#/components/schemas/PersonalApiTokenScope"
        expires_at:
          oneOf:
            - $ref: "#/components/schemas/Timestamp"
            - type: "null"
    PersonalApiTokenCreated:
      type: object
      additionalProperties: false
      required:
        - token
        - item
      properties:
        token:
          type: string
          minLength: 47
          maxLength: 512
          writeOnly: true
        item:
          $ref: "#/components/schemas/PersonalApiToken"
    DeveloperWorkspace:
      type: object
      additionalProperties: false
      required:
        - name
        - slug
        - role
      properties:
        id:
          $ref: "#/components/schemas/Identifier"
          description: Present for the current Workspace so co-versioned clients can address Workspace-scoped APIs.
        name:
          type: string
          minLength: 1
          maxLength: 100
        slug:
          type: string
          minLength: 1
          maxLength: 63
          pattern: ^[a-z0-9]+(?:-[a-z0-9]+)*$
        role:
          $ref: "#/components/schemas/Role"
    DeveloperWorkspaceList:
      type: object
      additionalProperties: false
      required:
        - items
        - has_more
      properties:
        items:
          type: array
          maxItems: 100
          items:
            $ref: "#/components/schemas/DeveloperWorkspace"
        next_cursor:
          oneOf:
            - $ref: "#/components/schemas/Cursor"
            - type: "null"
        has_more:
          type: boolean
    DeveloperWorkspaceSwitchRequest:
      type: object
      additionalProperties: false
      required:
        - slug
      properties:
        slug:
          type: string
          minLength: 1
          maxLength: 63
          pattern: ^[a-z0-9]+(?:-[a-z0-9]+)*$
    DeveloperWorkspaceSwitchResult:
      type: object
      additionalProperties: false
      required:
        - workspace
        - access_token
        - refresh_token
        - token_type
        - expires_in
      properties:
        workspace:
          $ref: "#/components/schemas/DeveloperWorkspace"
        access_token:
          type: string
          minLength: 50
          maxLength: 128
          format: password
        refresh_token:
          type: string
          minLength: 50
          maxLength: 128
          format: password
        token_type:
          type: string
          const: Bearer
        expires_in:
          type: integer
          const: 900
    CliProject:
      type: object
      additionalProperties: false
      required:
        - code
        - name
        - path
        - revision
        - url
      properties:
        code:
          $ref: "#/components/schemas/ProjectKey"
        name:
          type: string
          minLength: 1
          maxLength: 100
        path:
          type: string
          minLength: 1
          maxLength: 100
        revision:
          type: integer
          minimum: 1
        url:
          type: string
          format: uri
    CliProjectList:
      type: object
      additionalProperties: false
      required:
        - items
        - has_more
      properties:
        items:
          type: array
          maxItems: 100
          items:
            $ref: "#/components/schemas/CliProject"
        next_cursor:
          oneOf:
            - $ref: "#/components/schemas/Cursor"
            - type: "null"
        has_more:
          type: boolean
    CliDocument:
      type: object
      additionalProperties: false
      required:
        - path
        - title
        - visibility
        - revision
        - url
        - truncated
      properties:
        path:
          type: string
          minLength: 1
          maxLength: 200
        title:
          type: string
          minLength: 1
          maxLength: 200
        visibility:
          type: string
          enum:
            - workspace
            - project
            - public
        revision:
          type: integer
          minimum: 1
        url:
          type: string
          format: uri
        body:
          type: string
          maxLength: 65536
        truncated:
          type: boolean
    CliDocumentList:
      type: object
      additionalProperties: false
      required:
        - items
        - has_more
      properties:
        items:
          type: array
          maxItems: 100
          items:
            $ref: "#/components/schemas/CliDocument"
        next_cursor:
          oneOf:
            - $ref: "#/components/schemas/Cursor"
            - type: "null"
        has_more:
          type: boolean
    CliTask:
      type: object
      additionalProperties: false
      required:
        - code
        - project_code
        - title
        - status_category
        - revision
        - url
      properties:
        code:
          $ref: "#/components/schemas/TaskCode"
        project_code:
          $ref: "#/components/schemas/ResourceCode"
        title:
          type: string
          minLength: 1
          maxLength: 500
        status_category:
          $ref: "#/components/schemas/StatusCategory"
        revision:
          type: integer
          minimum: 1
        url:
          type: string
          format: uri
    CliTaskCreateRequest:
      type: object
      additionalProperties: false
      required:
        - title
      properties:
        title:
          type: string
          minLength: 1
          maxLength: 500
        description_markdown:
          type: string
          maxLength: 262144
          description: Optional description in Assign's supported safe Markdown profile.
        priority:
          $ref: "#/components/schemas/TaskPriority"
    CliTaskList:
      type: object
      additionalProperties: false
      required:
        - items
        - has_more
      properties:
        items:
          type: array
          maxItems: 100
          items:
            $ref: "#/components/schemas/CliTask"
        next_cursor:
          oneOf:
            - $ref: "#/components/schemas/Cursor"
            - type: "null"
        has_more:
          type: boolean
    CliComment:
      type: object
      additionalProperties: false
      description: A canonical Task comment projected as supported Markdown. Deleted comments remain as empty-body tombstones.
      required:
        - body_markdown
        - actor
        - actor_name
        - revision
        - created_at
      properties:
        body_markdown:
          type: string
          maxLength: 262144
        actor:
          type: string
          enum:
            - user
            - user:delegated_connection
            - user:mcp
            - user:automation
            - user:external_actor
            - agent
            - agent:delegated_connection
            - agent:mcp
            - agent:automation
            - agent:external_actor
            - service
            - service:delegated_connection
            - service:mcp
            - service:automation
            - service:external_actor
        actor_name:
          oneOf:
            - type: string
              minLength: 1
              maxLength: 100
            - type: "null"
        revision:
          type: integer
          minimum: 1
        created_at:
          $ref: "#/components/schemas/Timestamp"
    CliCommentCreateRequest:
      type: object
      additionalProperties: false
      required:
        - markdown
      properties:
        markdown:
          type: string
          minLength: 1
          maxLength: 262144
    CliCommentList:
      type: object
      additionalProperties: false
      required:
        - items
        - has_more
      properties:
        items:
          type: array
          maxItems: 100
          items:
            $ref: "#/components/schemas/CliComment"
        next_cursor:
          oneOf:
            - $ref: "#/components/schemas/Cursor"
            - type: "null"
        has_more:
          type: boolean
    CliTaskMutationResult:
      type: object
      additionalProperties: false
      required:
        - task
        - reassigned
      properties:
        task:
          $ref: "#/components/schemas/CliTask"
        reassigned:
          type: boolean
          description: True only when the command closed a different current assignment period.
    CliSearchResult:
      type: object
      additionalProperties: false
      required:
        - resource_type
        - code
        - title
        - url
        - updated_at
      properties:
        resource_type:
          $ref: "#/components/schemas/CliSearchResourceType"
        code:
          $ref: "#/components/schemas/ResourceCode"
        project_code:
          oneOf:
            - $ref: "#/components/schemas/ResourceCode"
            - type: "null"
        title:
          type: string
          minLength: 1
          maxLength: 500
        url:
          type: string
          format: uri
        updated_at:
          $ref: "#/components/schemas/Timestamp"
    CliSearchResultList:
      type: object
      additionalProperties: false
      required:
        - items
        - has_more
      properties:
        items:
          type: array
          maxItems: 100
          items:
            $ref: "#/components/schemas/CliSearchResult"
        next_cursor:
          oneOf:
            - $ref: "#/components/schemas/Cursor"
            - type: "null"
        has_more:
          type: boolean
    CliTaskContext:
      type: object
      additionalProperties: false
      required:
        - task_code
        - profile
        - markdown
        - truncated
      properties:
        task_code:
          $ref: "#/components/schemas/TaskCode"
        profile:
          $ref: "#/components/schemas/CliTaskContextProfile"
        markdown:
          type: string
          maxLength: 65536
        truncated:
          type: boolean
        omissions:
          type: array
          maxItems: 100
          items:
            type: string
            enum:
              - inaccessible
              - unavailable
              - unsupported
              - truncated
    SearchResult:
      type: object
      additionalProperties: false
      required:
        - resource_type
        - resource_id
        - project_id
        - task_id
        - status_id
        - status_label
        - status_category
        - status_type
        - status_icon
        - status_color
        - title
        - snippet
        - updated_at
      properties:
        identifier:
          type: string
          description: Canonical ticket reference for Tasks and Comments, Project code, or Document path. Available independently of client collection loading.
        project_path:
          type: string
          description: Canonical Project path for navigation and parent context.
        project_code:
          type: string
          description: Recognizable parent Project code.
        resource_type:
          $ref: "#/components/schemas/SearchResourceType"
        resource_id:
          $ref: "#/components/schemas/Identifier"
        project_id:
          oneOf:
            - $ref: "#/components/schemas/Identifier"
            - type: "null"
          description: Parent Project for a Task, Document, or Comment result; `null` for a Project result.
        task_id:
          oneOf:
            - $ref: "#/components/schemas/Identifier"
            - type: "null"
          description: Parent Task for a Comment result; `null` otherwise.
        status_id:
          oneOf:
            - $ref: "#/components/schemas/Identifier"
            - type: "null"
          description: Current Status for a Task result; null for every other result type.
        status_label:
          oneOf:
            - type: string
              minLength: 1
              maxLength: 100
            - type: "null"
        status_category:
          oneOf:
            - $ref: "#/components/schemas/StatusCategory"
            - type: "null"
        status_type:
          oneOf:
            - $ref: "#/components/schemas/StatusType"
            - type: "null"
        status_icon:
          oneOf:
            - type: string
              pattern: ^[a-z][a-z0-9-]{0,63}$
            - type: "null"
        status_color:
          oneOf:
            - type: string
              pattern: ^#[0-9A-Fa-f]{6}$
            - type: "null"
        title:
          type: string
          minLength: 1
          maxLength: 500
        snippet:
          type: string
          maxLength: 240
          description: Bounded plain-text context from the indexed content. Treat as text, not HTML.
        subtitle:
          type: string
          maxLength: 100
          description: Workspace-safe supporting text. Person results contain the Workspace role.
        updated_at:
          $ref: "#/components/schemas/Timestamp"
    WorkspacePersonProfile:
      type: object
      additionalProperties: false
      required:
        - user_id
        - actor_id
        - display_name
        - username
        - workspace_role
        - status
        - is_you
        - title
        - bio
        - profile_status
        - profile_picture_url
        - avatar_initials
        - avatar_color
      properties:
        user_id:
          $ref: "#/components/schemas/Identifier"
        actor_id:
          $ref: "#/components/schemas/Identifier"
        display_name:
          type: string
          minLength: 1
          maxLength: 100
        username:
          $ref: "#/components/schemas/PublicUsername"
        workspace_role:
          type: string
          enum:
            - owner
            - admin
            - member
            - viewer
        status:
          type: string
          enum:
            - active
        is_you:
          type: boolean
        title:
          oneOf:
            - type: string
              minLength: 1
              maxLength: 100
            - type: "null"
          description: Job title or role from Account Profile settings; null when unset.
        bio:
          oneOf:
            - type: string
              maxLength: 500
            - type: "null"
          description: Short bio from Account Profile settings; null when unset.
        profile_status:
          oneOf:
            - type: string
              maxLength: 100
            - type: "null"
          description: Custom status text from Account Profile settings (distinct from the membership `status`); null when unset.
        profile_picture_url:
          oneOf:
            - type: string
              maxLength: 2048
            - type: "null"
          description: Workspace-authorized uploaded image route or external HTTPS Account Profile image; null when unset.
        avatar_initials:
          oneOf:
            - type: string
              minLength: 1
              maxLength: 2
            - type: "null"
          description: Account-owned avatar letters override; null derives letters from the display name.
        avatar_color:
          oneOf:
            - $ref: "#/components/schemas/PaletteColor"
            - type: "null"
          description: Account-owned avatar background; null predates AVATAR-001 and falls back to a deterministic palette pick.
    SearchResultList:
      type: object
      additionalProperties: false
      required:
        - items
        - has_more
      properties:
        result_version:
          type: string
          pattern: ^[a-f0-9]{64}$
          description: Opaque version of the first 20 unfiltered non-archived work results, scoped to query and current member. Absent for other filters/page sizes/cursors. Does not include People or prove exhaustive coverage. Discuss rechecks this version on explicit Send.
        items:
          type: array
          items:
            $ref: "#/components/schemas/SearchResult"
        next_cursor:
          oneOf:
            - $ref: "#/components/schemas/Cursor"
            - type: "null"
        has_more:
          type: boolean
    ErrorResponse:
      type: object
      additionalProperties: false
      required:
        - error
      properties:
        error:
          $ref: "#/components/schemas/Error"
    Error:
      type: object
      additionalProperties: false
      required:
        - code
        - message
      properties:
        code:
          type: string
          minLength: 1
          maxLength: 100
          pattern: ^[a-z][a-z0-9_]*$
        message:
          type: string
          minLength: 1
          maxLength: 500
        request_id:
          type: string
          minLength: 1
          maxLength: 200
        details:
          type: object
          description: Optional bounded machine metadata. Profile and Task validation may include field and reason; clients must tolerate unknown reasons and use message as the fallback.
          additionalProperties:
            type: string
    Role:
      type: string
      description: |
        `viewer` reads the Workspace and its work and writes nothing. `admin`
        holds the same capabilities as `owner`; the only owner-reserved
        operation is archiving the Workspace, and a Workspace must always
        retain at least one active owner.
      enum:
        - owner
        - admin
        - member
        - viewer
    WorkspaceRef:
      type: object
      additionalProperties: false
      required:
        - id
        - name
        - slug
      properties:
        id:
          $ref: "#/components/schemas/Identifier"
        name:
          type: string
        slug:
          type: string
    InterfaceLanguageState:
      type: object
      additionalProperties: false
      required:
        - language
        - source
        - direction
        - availability_revision
        - hint_revision
        - supported_languages
      description: |
        Upcoming. Core-qualified presentation language. Desired Account preference wins;
        Automatic matches ordered device languages through finite aliases, then falls back to English.
        Unavailable stored preferences are retained and resolve safely to English.
      properties:
        language:
          type: string
          minLength: 2
          maxLength: 35
        source:
          type: string
          enum:
            - account
            - device
            - fallback
        direction:
          type: string
          enum:
            - ltr
            - rtl
        reason:
          type: string
          enum:
            - language_unavailable
            - no_supported_device_language
        availability_revision:
          type: string
          maxLength: 100
        hint_revision:
          type: integer
          format: int64
          minimum: 1
        supported_languages:
          type: array
          maxItems: 100
          items:
            type: string
            minLength: 2
            maxLength: 35
    DeviceLanguageReport:
      type: object
      additionalProperties: false
      required:
        - languages
      properties:
        languages:
          type: array
          maxItems: 10
          description: Ordered BCP 47 tags, or an empty list for English fallback. Raw tags are not persisted.
          items:
            type: string
            minLength: 1
            maxLength: 35
    Me:
      type: object
      additionalProperties: false
      required:
        - id
        - email
        - display_name
        - username
        - profile_picture_url
        - title
        - phone
        - bio
        - status
        - timezone
        - timezone_inherited
        - locale
        - locale_inherited
        - date_format
        - time_format
        - number_format
        - voice_language
        - voice_spoken_responses
        - voice_confirmation
        - voice_show_transcript
        - editor_controls
        - totp_enabled
        - name_display
        - first_day_of_week
        - first_day_of_week_inherited
        - avatar_initials
        - avatar_color
      properties:
        interface_language_state:
          $ref: "#/components/schemas/InterfaceLanguageState"
        id:
          $ref: "#/components/schemas/Identifier"
        email:
          type: string
          format: email
        display_name:
          type: string
        username:
          $ref: "#/components/schemas/PublicUsername"
        profile_picture_url:
          oneOf:
            - type: string
              maxLength: 2048
              description: HTTPS legacy URL or authenticated stable API content path for an S3-hosted picture.
            - type: "null"
        avatar_initials:
          oneOf:
            - type: string
              minLength: 1
              maxLength: 2
            - type: "null"
          description: Account-owned avatar letters override; null derives letters from the display name.
        avatar_color:
          oneOf:
            - $ref: "#/components/schemas/PaletteColor"
            - type: "null"
          description: Account-owned avatar background, a random family name at registration; null predates AVATAR-001.
        title:
          oneOf:
            - type: string
              minLength: 1
              maxLength: 100
            - type: "null"
        phone:
          oneOf:
            - type: string
              pattern: ^\+[1-9][0-9]{7,14}$
            - type: "null"
        bio:
          oneOf:
            - type: string
              maxLength: 500
            - type: "null"
        status:
          oneOf:
            - type: string
              maxLength: 100
            - type: "null"
        timezone:
          type: string
          maxLength: 64
        timezone_inherited:
          type: boolean
          description: When true, timezone is the effective value from the current Workspace default.
        interface_language:
          type: string
          minLength: 1
          maxLength: 35
          default: automatic
          description: Desired Account interface language, independent of regional locale and Workspace defaults. Older servers may omit this field; clients use automatic. Withdrawn languages may remain stored.
        locale:
          type: string
          maxLength: 35
        locale_inherited:
          type: boolean
          description: When true, locale is the effective value from the current Workspace default.
        date_format:
          type: string
          enum:
            - locale
            - mdy
            - dmy
            - ymd
        time_format:
          type: string
          enum:
            - locale
            - 12h
            - 24h
        number_format:
          type: string
          enum:
            - locale
            - decimal_comma
            - decimal_point
        voice_language:
          type: string
          maxLength: 35
        voice_spoken_responses:
          type: string
          enum:
            - off
            - confirmations
            - full
        voice_confirmation:
          type: string
          enum:
            - standard
            - confirm_more
        voice_show_transcript:
          type: boolean
        totp_enabled:
          type: boolean
          description: True only when the current account has a confirmed TOTP authenticator credential.
        editor_controls:
          type: string
          enum:
            - contextual
            - persistent
          description: Account-wide presentation preference for contextual or always-visible formatting controls across every editor host.
        name_display:
          type: string
          enum:
            - username
            - full_name
        first_day_of_week:
          type: string
          enum:
            - sunday
            - monday
        first_day_of_week_inherited:
          type: boolean
          description: When true, first_day_of_week is the effective value from the current Workspace default.
        workspace:
          $ref: "#/components/schemas/WorkspaceRef"
        actor_id:
          $ref: "#/components/schemas/Identifier"
        role:
          $ref: "#/components/schemas/Role"
        authorization_revision:
          allOf:
            - $ref: "#/components/schemas/AuthorizationRevision"
            - description: Opaque Workspace authorization epoch for the returned actor. It changes whenever Workspace membership, Project visibility or Project membership can change the actor's visible private data. Clients may use an exact match to admit an actor-and-Workspace-bound display cache; they must not derive permissions from this value.
    CurrentWorkTask:
      type: object
      additionalProperties: false
      required:
        - task_id
        - workspace_id
        - workspace_name
        - workspace_slug
        - project_id
        - project_name
        - project_path
        - project_key
        - task_number
        - task_code
        - title
        - status_id
        - status_label
        - status_category
        - updated_at
      properties:
        task_id:
          $ref: "#/components/schemas/Identifier"
        workspace_id:
          $ref: "#/components/schemas/Identifier"
        workspace_name:
          type: string
        workspace_slug:
          type: string
        project_id:
          $ref: "#/components/schemas/Identifier"
        project_name:
          type: string
        project_path:
          type: string
        project_key:
          type: string
        task_number:
          type: integer
          format: int64
          minimum: 1
        task_code:
          type: string
          description: Human-readable project-key and task-number identifier.
        title:
          type: string
        status_id:
          $ref: "#/components/schemas/Identifier"
        status_label:
          type: string
        status_category:
          type: string
          enum:
            - in_progress
            - in_review
        updated_at:
          type: string
          format: date-time
    CurrentWorkState:
      type: object
      additionalProperties: false
      required:
        - current
        - revision
        - updated_at
        - has_eligible_tasks
      properties:
        current:
          oneOf:
            - $ref: "#/components/schemas/CurrentWorkTask"
            - type: "null"
        revision:
          type: integer
          format: int64
          minimum: 1
        updated_at:
          type: string
          format: date-time
        has_eligible_tasks:
          type: boolean
    CurrentWorkCandidateList:
      type: object
      additionalProperties: false
      required:
        - items
      properties:
        items:
          type: array
          maxItems: 25
          items:
            $ref: "#/components/schemas/CurrentWorkTask"
    SetCurrentTaskRequest:
      type: object
      additionalProperties: false
      required:
        - task_id
      properties:
        task_id:
          $ref: "#/components/schemas/Identifier"
    UpdateProfileRequest:
      type: object
      additionalProperties: false
      minProperties: 1
      properties:
        display_name:
          type: string
          minLength: 1
          maxLength: 100
          description: |
            The user's full display name, trimmed before validation.
        username:
          type: string
          minLength: 3
          maxLength: 30
          pattern: ^[a-z0-9](?:[a-z0-9.]*[a-z0-9])?$
          description: Lowercased after trimming and unique across accounts. Only letters, numbers, and interior dots are accepted.
        profile_picture_url:
          type: string
          maxLength: 2048
          description: An HTTPS image URL. An empty string removes the picture.
        avatar_initials:
          type: string
          maxLength: 2
          description: Avatar letters override, 1–2 characters stored uppercased. An empty string clears the override and returns to name-derived letters.
        avatar_color:
          $ref: "#/components/schemas/PaletteColor"
        title:
          type: string
          maxLength: 100
          description: Optional job title or role. An empty string removes it.
        phone:
          type: string
          maxLength: 16
          description: Optional E.164 profile-only phone number; empty removes it. It is not an authentication or notification destination.
        bio:
          type: string
          maxLength: 500
        status:
          type: string
          maxLength: 100
        name_display:
          type: string
          enum:
            - username
            - full_name
          description: Which account name is preferred on compact identity surfaces. Username requires a selected username.
        first_day_of_week:
          type: string
          enum:
            - sunday
            - monday
        first_day_of_week_inherited:
          type: boolean
          description: Set true to use the current Workspace default; set false to keep a concrete Account value.
        timezone:
          type: string
          minLength: 1
          maxLength: 64
          description: Optional IANA timezone used for calendar-bound personal Work views. Invalid names are rejected; the server never guesses a timezone.
        timezone_inherited:
          type: boolean
          description: Set true to use the current Workspace default; set false to keep a concrete Account value.
        interface_language:
          type: string
          enum:
            - automatic
            - en
          description: Account interface language. Automatic follows qualified device preferences with English fallback. Only qualified public languages are writable; sample catalogs are not admitted. Values are trimmed and case-normalized.
        locale:
          type: string
          maxLength: 35
        locale_inherited:
          type: boolean
          description: Set true to use the current Workspace default; set false to keep a concrete Account value.
        date_format:
          type: string
          enum:
            - locale
            - mdy
            - dmy
            - ymd
        time_format:
          type: string
          enum:
            - locale
            - 12h
            - 24h
        number_format:
          type: string
          enum:
            - locale
            - decimal_comma
            - decimal_point
        voice_language:
          type: string
          maxLength: 35
        voice_spoken_responses:
          type: string
          enum:
            - off
            - confirmations
            - full
        voice_confirmation:
          type: string
          enum:
            - standard
            - confirm_more
        voice_show_transcript:
          type: boolean
        editor_controls:
          type: string
          enum:
            - contextual
            - persistent
          description: Selects contextual or persistent formatting controls across Document, Task-description, and Comment editors.
    SessionSummary:
      type: object
      additionalProperties: false
      required:
        - id
        - current
        - created_at
        - authenticated_at
        - last_seen_at
        - idle_expires_at
        - absolute_expires_at
      properties:
        id:
          $ref: "#/components/schemas/Identifier"
        workspace_id:
          $ref: "#/components/schemas/Identifier"
        current:
          type: boolean
          description: True for the session making this request.
        created_at:
          type: string
          format: date-time
        authenticated_at:
          type: string
          format: date-time
          description: |
            When credentials were last actually presented. Idle extension
            does not move it, so it is the value the 15-minute
            recent-authentication window is measured against.
        last_seen_at:
          type: string
          format: date-time
        idle_expires_at:
          type: string
          format: date-time
        absolute_expires_at:
          type: string
          format: date-time
    SessionList:
      type: object
      additionalProperties: false
      required:
        - items
        - has_more
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/SessionSummary"
        next_cursor:
          oneOf:
            - $ref: "#/components/schemas/Cursor"
            - type: "null"
        has_more:
          type: boolean
    McpGrantWorkspace:
      type: object
      additionalProperties: false
      required:
        - id
        - name
      properties:
        id:
          $ref: "#/components/schemas/Identifier"
        name:
          type: string
          minLength: 1
          maxLength: 100
    McpGrant:
      type: object
      additionalProperties: false
      required:
        - id
        - client_id
        - client_name
        - client_family
        - resource
        - scopes
        - workspaces
        - created_at
        - last_used_at
        - absolute_expires_at
      properties:
        id:
          $ref: "#/components/schemas/Identifier"
        client_id:
          type: string
          minLength: 16
          maxLength: 512
        client_name:
          type: string
          minLength: 1
          maxLength: 100
        client_family:
          type: string
          enum:
            - openai
            - claude
            - other
        resource:
          type: string
          format: uri
          enum:
            - https://mcp.assign.so/
        scopes:
          type: array
          minItems: 1
          maxItems: 37
          uniqueItems: true
          items:
            type: string
            enum:
              - assign:activity:read
              - assign:agents:read
              - assign:agents:write
              - assign:attachments:read
              - assign:attachments:write
              - assign:code:read
              - assign:comments:read
              - assign:comments:write
              - assign:context:read
              - assign:discuss:read
              - assign:discuss:write
              - assign:documents:read
              - assign:documents:write
              - assign:inbox:read
              - assign:inbox:write
              - assign:knowledge:read
              - assign:knowledge:write
              - assign:labels:read
              - assign:labels:write
              - assign:members:read
              - assign:memory:read
              - assign:memory:write
              - assign:milestones:read
              - assign:milestones:write
              - assign:operations:read
              - assign:operations:write
              - assign:projects:read
              - assign:projects:write
              - assign:read
              - assign:relations:read
              - assign:relations:write
              - assign:search:read
              - assign:statuses:read
              - assign:tasks:read
              - assign:tasks:write
              - assign:workspaces:read
              - assign:write
        workspaces:
          type: array
          minItems: 1
          items:
            $ref: "#/components/schemas/McpGrantWorkspace"
        created_at:
          type: string
          format: date-time
        last_used_at:
          type: string
          format: date-time
        absolute_expires_at:
          type: string
          format: date-time
    McpGrantList:
      type: object
      additionalProperties: false
      required:
        - items
        - has_more
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/McpGrant"
        next_cursor:
          oneOf:
            - $ref: "#/components/schemas/Cursor"
            - type: "null"
        has_more:
          type: boolean
    WorkspaceAiAccessPolicy:
      type: object
      additionalProperties: false
      required:
        - enabled
        - revision
        - updated_at
      properties:
        enabled:
          type: boolean
        revision:
          type: integer
          format: int64
          minimum: 0
        updated_at:
          oneOf:
            - type: string
              format: date-time
            - type: "null"
    WorkspaceAiAccessPolicyUpdateRequest:
      type: object
      additionalProperties: false
      required:
        - enabled
      properties:
        enabled:
          type: boolean
    McpServiceCredential:
      type: object
      additionalProperties: false
      required:
        - id
        - actor_id
        - prefix
        - name
        - scopes
        - project_ids
        - created_at
        - last_used_at
        - expires_at
      properties:
        id:
          $ref: "#/components/schemas/Identifier"
        actor_id:
          $ref: "#/components/schemas/Identifier"
        prefix:
          type: string
          minLength: 8
          maxLength: 32
          description: Non-secret display prefix; never sufficient to authenticate.
        name:
          type: string
          minLength: 1
          maxLength: 100
        scopes:
          type: array
          minItems: 1
          maxItems: 37
          uniqueItems: true
          items:
            type: string
            enum:
              - assign:activity:read
              - assign:agents:read
              - assign:agents:write
              - assign:attachments:read
              - assign:attachments:write
              - assign:code:read
              - assign:comments:read
              - assign:comments:write
              - assign:context:read
              - assign:discuss:read
              - assign:discuss:write
              - assign:documents:read
              - assign:documents:write
              - assign:inbox:read
              - assign:inbox:write
              - assign:knowledge:read
              - assign:knowledge:write
              - assign:labels:read
              - assign:labels:write
              - assign:members:read
              - assign:memory:read
              - assign:memory:write
              - assign:milestones:read
              - assign:milestones:write
              - assign:operations:read
              - assign:operations:write
              - assign:projects:read
              - assign:projects:write
              - assign:read
              - assign:relations:read
              - assign:relations:write
              - assign:search:read
              - assign:statuses:read
              - assign:tasks:read
              - assign:tasks:write
              - assign:workspaces:read
              - assign:write
        project_ids:
          type: array
          maxItems: 100
          uniqueItems: true
          items:
            $ref: "#/components/schemas/Identifier"
        created_at:
          type: string
          format: date-time
        last_used_at:
          oneOf:
            - type: string
              format: date-time
            - type: "null"
        expires_at:
          type: string
          format: date-time
    McpServiceCredentialCreateRequest:
      type: object
      additionalProperties: false
      required:
        - name
        - scopes
        - project_ids
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 100
        scopes:
          type: array
          minItems: 1
          maxItems: 37
          uniqueItems: true
          items:
            type: string
            enum:
              - assign:activity:read
              - assign:agents:read
              - assign:agents:write
              - assign:attachments:read
              - assign:attachments:write
              - assign:code:read
              - assign:comments:read
              - assign:comments:write
              - assign:context:read
              - assign:discuss:read
              - assign:discuss:write
              - assign:documents:read
              - assign:documents:write
              - assign:inbox:read
              - assign:inbox:write
              - assign:knowledge:read
              - assign:knowledge:write
              - assign:labels:read
              - assign:labels:write
              - assign:members:read
              - assign:memory:read
              - assign:memory:write
              - assign:milestones:read
              - assign:milestones:write
              - assign:operations:read
              - assign:operations:write
              - assign:projects:read
              - assign:projects:write
              - assign:read
              - assign:relations:read
              - assign:relations:write
              - assign:search:read
              - assign:statuses:read
              - assign:tasks:read
              - assign:tasks:write
              - assign:workspaces:read
              - assign:write
        project_ids:
          type: array
          maxItems: 100
          uniqueItems: true
          items:
            $ref: "#/components/schemas/Identifier"
        expires_at:
          type: string
          format: date-time
          description: Optional expiry no more than one year after creation; defaults to 90 days.
    McpServiceCredentialCreated:
      type: object
      additionalProperties: false
      required:
        - token
        - item
      properties:
        token:
          type: string
          minLength: 50
          maxLength: 64
          writeOnly: true
          description: One-time raw bearer credential. It is never returned again.
        item:
          $ref: "#/components/schemas/McpServiceCredential"
    McpServiceCredentialList:
      type: object
      additionalProperties: false
      required:
        - items
      properties:
        items:
          type: array
          maxItems: 100
          items:
            $ref: "#/components/schemas/McpServiceCredential"
    RevokedSessions:
      type: object
      additionalProperties: false
      required:
        - revoked_count
      properties:
        revoked_count:
          type: integer
          minimum: 0
          description: |
            Sessions whose state actually changed. A repeated call reports
            zero rather than failing.
    LinkedIdentity:
      type: object
      additionalProperties: false
      required:
        - id
        - provider
        - provider_email_verified
        - created_at
      properties:
        id:
          $ref: "#/components/schemas/Identifier"
        provider:
          type: string
          enum:
            - google
            - github
            - apple
        provider_email:
          oneOf:
            - type: string
              format: email
            - type: "null"
          description: |
            The provider's normalized email, when it disclosed one. An Apple
            private-relay address is stored as given and is not assumed to be
            a stable real-world mailbox.
        provider_email_verified:
          type: boolean
          description: |
            Whether the provider asserted the address as verified. An
            unverified or absent address can never establish ownership of an
            existing Assign account.
        created_at:
          type: string
          format: date-time
    LinkedIdentityList:
      type: object
      additionalProperties: false
      required:
        - items
        - has_more
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/LinkedIdentity"
        next_cursor:
          oneOf:
            - $ref: "#/components/schemas/Cursor"
            - type: "null"
        has_more:
          type: boolean
    StartIdentityLinkRequest:
      type: object
      additionalProperties: false
      required:
        - provider
      properties:
        provider:
          type: string
          enum:
            - google
            - github
            - apple
    IdentityLinkCeremony:
      type: object
      additionalProperties: false
      required:
        - authorization_url
        - expires_at
      properties:
        authorization_url:
          type: string
          format: uri
          description: |
            The provider authorization URL to send the user to. The ceremony
            state is minted server-side and bound to the calling user and
            session; the client neither generates nor modifies it.
        expires_at:
          type: string
          format: date-time
    Workspace:
      type: object
      additionalProperties: false
      required:
        - id
        - name
        - slug
        - icon_url
        - revision
        - created_at
        - updated_at
        - archived_at
      properties:
        id:
          $ref: "#/components/schemas/Identifier"
        name:
          type: string
          minLength: 1
          maxLength: 100
        slug:
          type: string
          description: |
            Server-generated canonical Workspace URL segment. Authorized
            Workspace managers can change it through the revisioned
            Workspace update operation.
        icon_url:
          oneOf:
            - type: string
              maxLength: 2048
              description: HTTPS legacy URL or authenticated stable API content path for an S3-hosted icon.
            - type: "null"
          description: Optional HTTPS Workspace icon; null uses the initials fallback.
        revision:
          type: integer
          format: int64
          minimum: 1
        archived_at:
          oneOf:
            - type: string
              format: date-time
            - type: "null"
          description: |
            When set, the Workspace is archived: no data was deleted, every
            session scoped to it was revoked, and its scoped routes answer
            not-found for everyone but an owner.
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    CreateWorkspaceRequest:
      type: object
      additionalProperties: false
      required:
        - name
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 100
          description: Trimmed before validation.
        slug:
          type: string
          minLength: 1
          maxLength: 63
          pattern: ^[a-z0-9]+(?:[a-z0-9-]*[a-z0-9])?$
          description: Optional unique canonical Workspace URL segment.
    WorkspaceCreationOptions:
      type: object
      additionalProperties: false
      required:
        - free_available
        - paid_available
        - paid_choices
      properties:
        free_available:
          type: boolean
          description: Whether this account may claim and create another active Free Workspace within its three-Workspace allowance.
        free_workspace:
          $ref: "#/components/schemas/WorkspaceClaimSummary"
        paid_available:
          type: boolean
          description: Whether at least one paid Workspace quote can currently be purchased.
        paid_choices:
          type: array
          maxItems: 20
          items:
            $ref: "#/components/schemas/WorkspaceCreationPaidChoice"
    WorkspaceClaimSummary:
      type: object
      additionalProperties: false
      required:
        - id
        - name
        - slug
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        slug:
          type: string
    WorkspaceCreationPaidChoice:
      type: object
      additionalProperties: false
      required:
        - quote_id
        - plan
        - label
        - cadence
      properties:
        quote_id:
          type: string
          maxLength: 255
        plan:
          type: string
          enum:
            - personal
            - team
            - growth
        label:
          type: string
          maxLength: 100
        cadence:
          type: string
          enum:
            - monthly
            - annual
    CreateWorkspaceCheckoutRequest:
      type: object
      additionalProperties: false
      required:
        - name
        - quote_id
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 100
        slug:
          type: string
          minLength: 1
          maxLength: 63
          pattern: ^[a-z0-9]+(?:[a-z0-9-]*[a-z0-9])?$
        quote_id:
          type: string
          minLength: 1
          maxLength: 255
    WorkspaceCheckout:
      type: object
      additionalProperties: false
      required:
        - id
        - state
        - expires_at
      properties:
        id:
          $ref: "#/components/schemas/Identifier"
        state:
          type: string
          enum:
            - pending
            - activating
            - activated
            - expired
            - canceled
            - failed
        expires_at:
          type: string
          format: date-time
        url:
          type: string
          format: uri
          maxLength: 2048
        workspace:
          $ref: "#/components/schemas/WorkspaceClaimSummary"
    BootstrapWorkspaceRequest:
      type: object
      additionalProperties: false
      required:
        - name
        - slug
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 100
          description: Trimmed before validation.
        slug:
          type: string
          minLength: 1
          maxLength: 63
          pattern: ^[a-z0-9]+(?:[a-z0-9-]*[a-z0-9])?$
          description: The unique canonical Workspace URL segment.
    UpdateWorkspaceRequest:
      type: object
      additionalProperties: false
      description: |
        Exactly one of `name`, `slug`, `icon_url`, or `archived` must be present.
        `archived` may only be `false`; archiving is the `DELETE` operation.
      minProperties: 1
      maxProperties: 1
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 100
        slug:
          type: string
          minLength: 1
          maxLength: 63
          pattern: ^[a-z0-9]+(?:[a-z0-9-]*[a-z0-9])?$
          description: The unique canonical Workspace URL segment.
        icon_url:
          type: string
          maxLength: 2048
          description: An HTTPS image URL. An empty string removes the icon.
        archived:
          type: boolean
          description: |
            Only `false` is accepted, which unarchives the Workspace.
            Archiving is `DELETE /api/v1/workspaces/{workspace_id}`, which is
            owner-only and requires recent authentication.
    Membership:
      type: object
      additionalProperties: false
      required:
        - id
        - workspace_id
        - user_id
        - role
        - state
        - revision
        - created_at
        - updated_at
      properties:
        id:
          $ref: "#/components/schemas/Identifier"
        workspace_id:
          $ref: "#/components/schemas/Identifier"
        user_id:
          $ref: "#/components/schemas/Identifier"
        role:
          $ref: "#/components/schemas/Role"
        state:
          type: string
          enum:
            - invited
            - active
            - suspended
            - removed
            - left
          description: |
            A membership that stops being `active` has its sessions in this
            Workspace revoked immediately. `removed` retains the row and the
            member's Actor so past work stays attributable.
        revision:
          type: integer
          format: int64
          minimum: 1
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    AddMemberRequest:
      type: object
      additionalProperties: false
      required:
        - user_id
      properties:
        user_id:
          $ref: "#/components/schemas/Identifier"
    UpdateMembershipRequest:
      type: object
      additionalProperties: false
      description: At least one field must be present; an omitted field is unchanged.
      minProperties: 1
      properties:
        role:
          $ref: "#/components/schemas/Role"
        state:
          type: string
          enum:
            - active
            - suspended
    Invitation:
      type: object
      additionalProperties: false
      required:
        - id
        - workspace_id
        - email
        - role
        - state
        - expires_at
        - revision
        - created_at
      properties:
        id:
          $ref: "#/components/schemas/Identifier"
        workspace_id:
          $ref: "#/components/schemas/Identifier"
        email:
          type: string
          format: email
          description: Normalized to lowercase when the invitation is created.
        role:
          type: string
          enum:
            - admin
            - member
            - viewer
        state:
          type: string
          enum:
            - pending
            - accepted
            - expired
            - revoked
          description: |
            Derived, not stored: `expired` is computed from `expires_at`, so a
            lapsed invitation and one already swept from storage read the same
            way.
        invited_by_actor_id:
          oneOf:
            - $ref: "#/components/schemas/Identifier"
            - type: "null"
        expires_at:
          type: string
          format: date-time
        accepted_at:
          oneOf:
            - type: string
              format: date-time
            - type: "null"
        revoked_at:
          oneOf:
            - type: string
              format: date-time
            - type: "null"
        revision:
          type: integer
          format: int64
          minimum: 1
        created_at:
          type: string
          format: date-time
    InvitationList:
      type: object
      additionalProperties: false
      required:
        - items
        - has_more
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/Invitation"
        next_cursor:
          oneOf:
            - $ref: "#/components/schemas/Cursor"
            - type: "null"
        has_more:
          type: boolean
    CreateInvitationRequest:
      type: object
      additionalProperties: false
      required:
        - email
        - role
      properties:
        email:
          type: string
          format: email
        role:
          type: string
          enum:
            - admin
            - member
            - viewer
          description: |
            `owner` is deliberately not invitable. Ownership carries the
            archive power and the last-owner invariant, and is conferred by a
            membership change on somebody already present.
    CreatedInvitation:
      type: object
      additionalProperties: false
      required:
        - invitation
        - token
      properties:
        invitation:
          $ref: "#/components/schemas/Invitation"
        token:
          type: string
          description: |
            The one-time invitation token. Only its hash is stored, so this is
            the only response that will ever contain it; a replayed
            `Idempotency-Key` returns this same body.
    AcceptInvitationRequest:
      type: object
      additionalProperties: false
      required:
        - token
      properties:
        token:
          type: string
    WorkspaceSettings:
      type: object
      additionalProperties: false
      required:
        - workspace_id
        - description
        - default_timezone
        - default_locale
        - default_first_day_of_week
        - default_project_visibility
        - default_task_status_category
        - default_task_priority
        - default_task_assignee
        - revision
        - updated_at
      properties:
        workspace_id:
          $ref: "#/components/schemas/Identifier"
        description:
          oneOf:
            - type: string
              maxLength: 500
            - type: "null"
        default_timezone:
          type: string
          maxLength: 64
        default_locale:
          type: string
          maxLength: 35
        default_first_day_of_week:
          type: string
          enum:
            - sunday
            - monday
        default_project_visibility:
          type: string
          enum:
            - workspace
            - private
        default_task_status_category:
          type: string
          enum:
            - backlog
            - todo
            - in_progress
        default_task_priority:
          type: string
          enum:
            - none
            - low
            - medium
            - high
            - urgent
        default_task_assignee:
          type: string
          enum:
            - unassigned
            - creator
        revision:
          type: integer
          format: int64
          minimum: 1
        updated_at:
          type: string
          format: date-time
    WorkspaceSettingsPatch:
      type: object
      additionalProperties: false
      minProperties: 1
      properties:
        description:
          oneOf:
            - type: string
              maxLength: 500
            - type: "null"
        default_timezone:
          type: string
          maxLength: 64
        default_locale:
          type: string
          maxLength: 35
        default_first_day_of_week:
          type: string
          enum:
            - sunday
            - monday
        default_project_visibility:
          type: string
          enum:
            - workspace
            - private
        default_task_status_category:
          type: string
          enum:
            - backlog
            - todo
            - in_progress
        default_task_priority:
          type: string
          enum:
            - none
            - low
            - medium
            - high
            - urgent
        default_task_assignee:
          type: string
          enum:
            - unassigned
            - creator
    KnowledgeSettings:
      type: object
      additionalProperties: false
      required:
        - workspace_id
        - enabled
        - available
        - status
        - index_task_descriptions
        - index_comments
        - index_attachments
        - index_external_urls
        - index_repositories
        - index_audio_video
        - session_learning
        - included_projects
        - applied_revision
        - status_observed_at
        - indexed_through
        - source_sequence
        - backlog
        - last_success_at
        - failure_code
        - entity_counts
        - revision
        - updated_at
      properties:
        workspace_id:
          $ref: "#/components/schemas/Identifier"
        enabled:
          type: boolean
          description: Desired enablement defaults to the active paid-plan Knowledge entitlement unless explicitly changed. Empty included_projects keeps indexing and retrieval inactive.
        available:
          type: boolean
          description: True only when the Workspace has the active workspace_knowledge entitlement.
        status:
          type: string
          enum:
            - off
            - provisioning
            - on
            - degraded
            - disabling
        index_task_descriptions:
          type: boolean
        index_comments:
          type: boolean
        index_attachments:
          type: boolean
        index_external_urls:
          type: boolean
        index_repositories:
          type: boolean
        index_audio_video:
          type: boolean
        session_learning:
          type: string
          enum:
            - off
            - ask
        included_projects:
          type: array
          maxItems: 100
          uniqueItems: true
          items:
            $ref: "#/components/schemas/Identifier"
        applied_revision:
          oneOf:
            - type: integer
              format: int64
              minimum: 1
            - type: "null"
        status_observed_at:
          oneOf:
            - type: string
              format: date-time
            - type: "null"
        indexed_through:
          type: integer
          format: int64
          minimum: 0
        source_sequence:
          type: integer
          format: int64
          minimum: 0
        backlog:
          type: integer
          format: int64
          minimum: 0
        last_success_at:
          oneOf:
            - type: string
              format: date-time
            - type: "null"
        failure_code:
          oneOf:
            - type: string
              maxLength: 80
            - type: "null"
        entity_counts:
          type: object
          additionalProperties:
            type: integer
            minimum: 0
        revision:
          type: integer
          format: int64
          minimum: 1
        updated_at:
          type: string
          format: date-time
    KnowledgeSettingsPatch:
      type: object
      additionalProperties: false
      minProperties: 1
      properties:
        enabled:
          type: boolean
          description: Saves an explicit enablement choice that survives billing changes. Enabling requires the active Knowledge entitlement and may precede Project selection.
        index_task_descriptions:
          type: boolean
        index_comments:
          type: boolean
        index_attachments:
          type: boolean
        index_external_urls:
          type: boolean
        index_repositories:
          type: boolean
        index_audio_video:
          type: boolean
        session_learning:
          type: string
          enum:
            - off
            - ask
        included_projects:
          type: array
          maxItems: 100
          uniqueItems: true
          items:
            $ref: "#/components/schemas/Identifier"
    KnowledgeRepository:
      type: object
      additionalProperties: false
      required:
        - id
        - workspace_id
        - installation_id
        - resource_id
        - provider
        - repository_name
        - branch
        - docs_root
        - sync_mode
        - status
        - webhook_health
        - revision
        - last_git_commit_sha
        - last_successful_sync_at
        - pending_documents
        - open_conflicts
      properties:
        id:
          $ref: "#/components/schemas/Identifier"
        workspace_id:
          $ref: "#/components/schemas/Identifier"
        installation_id:
          $ref: "#/components/schemas/Identifier"
        resource_id:
          $ref: "#/components/schemas/Identifier"
        provider:
          type: string
          enum:
            - github
            - gitlab
            - bitbucket
        repository_name:
          type: string
          minLength: 1
          maxLength: 300
        branch:
          type: string
          minLength: 1
          maxLength: 200
        docs_root:
          type: string
          minLength: 1
          maxLength: 500
        sync_mode:
          type: string
          enum:
            - outbound
            - bidirectional
        status:
          type: string
          enum:
            - initializing
            - importing
            - healthy
            - pending
            - syncing
            - conflict
            - degraded
            - failed
            - paused
            - disconnected
        webhook_health:
          type: string
          enum:
            - unknown
            - healthy
            - degraded
            - failed
        revision:
          type: integer
          format: int64
          minimum: 1
        last_git_commit_sha:
          oneOf:
            - type: string
              pattern: ^[0-9a-f]{40}([0-9a-f]{24})?$
            - type: "null"
        last_successful_sync_at:
          oneOf:
            - type: string
              format: date-time
            - type: "null"
        pending_documents:
          type: integer
          format: int64
          minimum: 0
        open_conflicts:
          type: integer
          format: int64
          minimum: 0
    KnowledgeRepositoryConnect:
      type: object
      additionalProperties: false
      required:
        - installation_id
        - resource_id
        - provider
        - repository_name
        - branch
        - docs_root
        - sync_mode
        - repository_readers_acknowledged
      properties:
        installation_id:
          $ref: "#/components/schemas/Identifier"
        resource_id:
          $ref: "#/components/schemas/Identifier"
        provider:
          type: string
          enum:
            - github
            - gitlab
            - bitbucket
        repository_name:
          type: string
          minLength: 1
          maxLength: 300
        branch:
          type: string
          minLength: 1
          maxLength: 200
        docs_root:
          type: string
          minLength: 1
          maxLength: 500
        sync_mode:
          type: string
          enum:
            - outbound
            - bidirectional
        repository_readers_acknowledged:
          type: boolean
          description: Must be true to acknowledge that repository readers can read every synchronized Document.
    KnowledgeRepositoryStateUpdate:
      type: object
      additionalProperties: false
      required:
        - state
        - revision
      properties:
        state:
          type: string
          enum:
            - paused
            - healthy
            - disconnected
        revision:
          type: integer
          format: int64
          minimum: 1
    KnowledgeRepositorySyncRun:
      type: object
      additionalProperties: false
      required:
        - id
        - direction
        - trigger
        - status
        - base_git_sha
        - result_git_sha
        - documents_created
        - documents_updated
        - documents_moved
        - documents_archived
        - documents_conflicted
        - error_code
        - started_at
        - completed_at
      properties:
        id:
          $ref: "#/components/schemas/Identifier"
        direction:
          type: string
          enum:
            - assign_to_git
            - git_to_assign
            - bidirectional
        trigger:
          type: string
          enum:
            - automatic
            - manual
            - webhook
            - initial_import
            - reimport
            - recovery
            - agent
        status:
          type: string
          enum:
            - running
            - successful
            - conflict
            - failed
            - cancelled
        base_git_sha:
          oneOf:
            - type: string
            - type: "null"
        result_git_sha:
          oneOf:
            - type: string
            - type: "null"
        documents_created:
          type: integer
          minimum: 0
        documents_updated:
          type: integer
          minimum: 0
        documents_moved:
          type: integer
          minimum: 0
        documents_archived:
          type: integer
          minimum: 0
        documents_conflicted:
          type: integer
          minimum: 0
        error_code:
          oneOf:
            - type: string
              maxLength: 80
            - type: "null"
        started_at:
          type: string
          format: date-time
        completed_at:
          oneOf:
            - type: string
              format: date-time
            - type: "null"
    KnowledgeRepositorySyncRunList:
      type: object
      additionalProperties: false
      required:
        - items
      properties:
        items:
          type: array
          maxItems: 100
          items:
            $ref: "#/components/schemas/KnowledgeRepositorySyncRun"
    KnowledgeRepositoryConflict:
      type: object
      additionalProperties: false
      required:
        - id
        - document_id
        - relative_path
        - status
        - observed_git_sha
        - expected_document_revision
        - created_at
      properties:
        id:
          $ref: "#/components/schemas/Identifier"
        document_id:
          $ref: "#/components/schemas/Identifier"
        relative_path:
          type: string
          minLength: 1
          maxLength: 1000
        status:
          type: string
          enum:
            - open
        observed_git_sha:
          type: string
        expected_document_revision:
          type: integer
          format: int64
          minimum: 1
        created_at:
          type: string
          format: date-time
    KnowledgeRepositoryConflictList:
      type: object
      additionalProperties: false
      required:
        - items
      properties:
        items:
          type: array
          maxItems: 100
          items:
            $ref: "#/components/schemas/KnowledgeRepositoryConflict"
    KnowledgeRepositoryConflictResolution:
      type: object
      additionalProperties: false
      required:
        - action
        - expected_git_sha
        - expected_document_revision
      properties:
        action:
          type: string
          enum:
            - keep_assign
            - use_git
            - merge
        expected_git_sha:
          type: string
          pattern: ^[0-9a-f]{40}([0-9a-f]{24})?$
        expected_document_revision:
          type: integer
          format: int64
          minimum: 1
        merged_markdown:
          type: string
          maxLength: 1048576
    AgentStudioBinding:
      type: object
      additionalProperties: false
      required:
        - kind
        - resource_type
        - contract_version
        - capability
        - configuration
      properties:
        id:
          $ref: "#/components/schemas/Identifier"
        kind:
          type: string
          enum:
            - knowledge
            - skill
            - tool
            - integration
        resource_type:
          type: string
          minLength: 1
          maxLength: 80
        resource_id:
          oneOf:
            - $ref: "#/components/schemas/Identifier"
            - type: "null"
        contract_version:
          type: string
          minLength: 1
          maxLength: 100
        capability:
          type: string
          enum:
            - observe
            - communicate
            - manage
        configuration:
          type: object
          additionalProperties: true
          maxProperties: 64
    CustomAgentDefinitionInput:
      type: object
      additionalProperties: false
      required:
        - slug
        - name
        - description
        - responsibility
        - instructions
        - state
        - bindings
      properties:
        slug:
          type: string
          pattern: ^[a-z0-9]+(?:-[a-z0-9]+)*$
          minLength: 2
          maxLength: 80
        name:
          type: string
          minLength: 1
          maxLength: 80
        description:
          type: string
          minLength: 1
          maxLength: 500
        responsibility:
          type: string
          minLength: 1
          maxLength: 500
        personality:
          type: string
          maxLength: 1000
          description: Customer-authored tone and interaction style.
        routing_description:
          type: string
          maxLength: 500
          description: Safe description used to select this Agent for a request; responsibility is used when omitted.
        routing_tags:
          type: array
          maxItems: 12
          uniqueItems: true
          items:
            type: string
            minLength: 1
            maxLength: 40
            pattern: ^[a-z0-9]+(?:[a-z0-9_-]*[a-z0-9])?$
        instructions:
          type: string
          minLength: 1
          maxLength: 8000
        conversation_starters:
          type: array
          maxItems: 6
          uniqueItems: true
          items:
            type: string
            minLength: 1
            maxLength: 200
        subagent_definition_ids:
          type: array
          maxItems: 8
          uniqueItems: true
          items:
            $ref: "#/components/schemas/Identifier"
          description: Same-Workspace custom Agent definitions this Agent may delegate to. Self-references and cycles are rejected.
        approval_policy:
          type: string
          enum:
            - manage_actions
            - every_action
          default: manage_actions
        prohibited_actions:
          type: array
          maxItems: 12
          uniqueItems: true
          items:
            type: string
            minLength: 1
            maxLength: 200
        trigger_types:
          type: array
          minItems: 1
          maxItems: 3
          uniqueItems: true
          items:
            $ref: "#/components/schemas/AgentTriggerType"
          default:
            - manual
        visibility:
          type: string
          enum:
            - workspace
          default: workspace
        state:
          type: string
          enum:
            - draft
            - active
            - paused
        mcp_enabled:
          type: boolean
          description: Allow permitted Assign MCP operations under this Agent identity. New definitions default to false; omitted updates preserve the current value. Saving false revokes operational access.
        bindings:
          type: array
          maxItems: 32
          items:
            $ref: "#/components/schemas/AgentStudioBinding"
    CustomAgentDefinition:
      type: object
      additionalProperties: false
      required:
        - contract_generation
        - workspace_id
        - definition_id
        - actor_id
        - revision
        - version_digest
        - slug
        - name
        - description
        - responsibility
        - instructions
        - state
        - bindings
      properties:
        contract_generation:
          type: string
          enum:
            - agent-participation.v1
        workspace_id:
          $ref: "#/components/schemas/Identifier"
        definition_id:
          $ref: "#/components/schemas/Identifier"
        actor_id:
          $ref: "#/components/schemas/Identifier"
        revision:
          type: integer
          format: int64
          minimum: 1
        version_digest:
          type: string
          pattern: ^sha256:[0-9a-f]{64}$
        updated_at:
          type: string
          pattern: ^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(?:[.][0-9]{1,9})?Z$
          description: UTC RFC3339 current catalog sort position when returned by the Studio list; absent from immutable revision snapshots and detail responses.
        slug:
          type: string
          pattern: ^[a-z0-9]+(?:-[a-z0-9]+)*$
          minLength: 2
          maxLength: 80
        name:
          type: string
          minLength: 1
          maxLength: 80
        description:
          type: string
          minLength: 1
          maxLength: 500
        responsibility:
          type: string
          minLength: 1
          maxLength: 500
        personality:
          type: string
          maxLength: 1000
        routing_description:
          type: string
          maxLength: 500
        routing_tags:
          type: array
          maxItems: 12
          uniqueItems: true
          items:
            type: string
            minLength: 1
            maxLength: 40
        instructions:
          type: string
          minLength: 1
          maxLength: 8000
        conversation_starters:
          type: array
          maxItems: 6
          uniqueItems: true
          items:
            type: string
            minLength: 1
            maxLength: 200
        subagent_definition_ids:
          type: array
          maxItems: 8
          uniqueItems: true
          items:
            $ref: "#/components/schemas/Identifier"
        approval_policy:
          type: string
          enum:
            - manage_actions
            - every_action
        prohibited_actions:
          type: array
          maxItems: 12
          uniqueItems: true
          items:
            type: string
            minLength: 1
            maxLength: 200
        trigger_types:
          type: array
          minItems: 1
          maxItems: 3
          uniqueItems: true
          items:
            $ref: "#/components/schemas/AgentTriggerType"
        visibility:
          type: string
          enum:
            - workspace
        state:
          type: string
          enum:
            - draft
            - active
            - paused
        mcp_enabled:
          type: boolean
          description: Allow permitted Assign MCP operations under this Agent identity. New definitions default to false; omitted updates preserve the current value. Saving false revokes operational access.
        bindings:
          type: array
          maxItems: 32
          items:
            $ref: "#/components/schemas/AgentStudioBinding"
    CustomAgentDefinitionList:
      type: object
      additionalProperties: false
      required:
        - items
        - has_more
      properties:
        items:
          type: array
          maxItems: 100
          items:
            $ref: "#/components/schemas/CustomAgentDefinition"
        next_cursor:
          oneOf:
            - $ref: "#/components/schemas/Cursor"
            - type: "null"
        has_more:
          type: boolean
    CustomAgentProfile:
      type: object
      additionalProperties: false
      required:
        - definition_id
        - actor_id
        - name
        - description
        - responsibility
        - manager_actor_id
        - personality
        - conversation_starters
        - knowledge_categories
        - capabilities
        - prohibited_actions
        - approval_boundary
        - trigger_types
        - state
        - revision
        - latest_outcome
      properties:
        definition_id:
          $ref: "#/components/schemas/Identifier"
        actor_id:
          $ref: "#/components/schemas/Identifier"
        name:
          type: string
          minLength: 1
          maxLength: 80
        description:
          type: string
          minLength: 1
          maxLength: 500
        responsibility:
          type: string
          minLength: 1
          maxLength: 500
        manager_actor_id:
          $ref: "#/components/schemas/Identifier"
        personality:
          type: string
          maxLength: 1000
        conversation_starters:
          type: array
          maxItems: 6
          items:
            type: string
            maxLength: 200
        knowledge_categories:
          type: array
          maxItems: 32
          items:
            type: string
            maxLength: 80
        capabilities:
          type: array
          maxItems: 32
          items:
            type: string
            maxLength: 200
        prohibited_actions:
          type: array
          maxItems: 12
          items:
            type: string
            maxLength: 200
        approval_boundary:
          type: string
          minLength: 1
          maxLength: 200
        trigger_types:
          type: array
          minItems: 1
          maxItems: 3
          items:
            $ref: "#/components/schemas/AgentTriggerType"
        state:
          type: string
          enum:
            - draft
            - active
            - paused
            - removed
        revision:
          type: integer
          format: int64
          minimum: 1
        last_started_at:
          $ref: "#/components/schemas/Timestamp"
        last_finished_at:
          $ref: "#/components/schemas/Timestamp"
        last_successful_at:
          $ref: "#/components/schemas/Timestamp"
        latest_outcome:
          type: string
          maxLength: 4000
    AgentParticipationSourceKind:
      type: string
      enum:
        - manual
        - test
        - mention
        - delegation
        - event
    AgentParticipationRequestInput:
      type: object
      additionalProperties: false
      required:
        - source_kind
        - source_id
        - instructions
        - preview_only
      properties:
        source_kind:
          $ref: "#/components/schemas/AgentParticipationSourceKind"
        source_id:
          $ref: "#/components/schemas/Identifier"
        task_id:
          $ref: "#/components/schemas/Identifier"
        instructions:
          type: string
          maxLength: 2000
          description: Original bounded request input for its requester and Studio managers; returned as an empty string in member-visible history.
        preview_only:
          type: boolean
          description: Must be true exactly when source_kind is test.
    AgentParticipationRequest:
      type: object
      additionalProperties: false
      required:
        - id
        - workspace_id
        - definition_id
        - definition_revision
        - version_digest
        - requested_by_actor_id
        - source_kind
        - source_id
        - instructions
        - preview_only
        - request_digest
        - state
        - safe_summary
        - reserved_credits
        - used_credits
        - created_at
      properties:
        id:
          $ref: "#/components/schemas/Identifier"
        workspace_id:
          $ref: "#/components/schemas/Identifier"
        definition_id:
          $ref: "#/components/schemas/Identifier"
        definition_revision:
          type: integer
          format: int64
          minimum: 1
        version_digest:
          type: string
          pattern: ^sha256:[0-9a-f]{64}$
        requested_by_actor_id:
          $ref: "#/components/schemas/Identifier"
        source_kind:
          $ref: "#/components/schemas/AgentParticipationSourceKind"
        source_id:
          $ref: "#/components/schemas/Identifier"
        task_id:
          $ref: "#/components/schemas/Identifier"
        instructions:
          type: string
          maxLength: 2000
        preview_only:
          type: boolean
        request_digest:
          type: string
          pattern: ^sha256:[0-9a-f]{64}$
        state:
          type: string
          enum:
            - queued
            - running
            - waiting
            - ready
            - completed
            - no_action
            - failed
            - cancelled
            - billing_blocked
        safe_summary:
          type: string
          maxLength: 4000
        reserved_credits:
          type: integer
          format: int64
          minimum: 0
        used_credits:
          type: integer
          format: int64
          minimum: 0
        created_at:
          $ref: "#/components/schemas/Timestamp"
        finished_at:
          $ref: "#/components/schemas/Timestamp"
    AgentParticipationRequestList:
      type: object
      additionalProperties: false
      required:
        - items
        - has_more
      properties:
        items:
          type: array
          maxItems: 100
          items:
            $ref: "#/components/schemas/AgentParticipationRequest"
        next_cursor:
          oneOf:
            - $ref: "#/components/schemas/Cursor"
            - type: "null"
        has_more:
          type: boolean
    CustomAgentUsage:
      type: object
      additionalProperties: false
      required:
        - definition_id
        - from
        - to
        - runs
        - pending_runs
        - reserved_credits
        - used_credits
        - reconciled_at
      properties:
        definition_id:
          $ref: "#/components/schemas/Identifier"
        from:
          $ref: "#/components/schemas/Timestamp"
        to:
          $ref: "#/components/schemas/Timestamp"
        runs:
          type: integer
          format: int64
          minimum: 0
        pending_runs:
          type: integer
          format: int64
          minimum: 0
        reserved_credits:
          type: integer
          format: int64
          minimum: 0
        used_credits:
          type: integer
          format: int64
          minimum: 0
        reconciled_at:
          $ref: "#/components/schemas/Timestamp"
    AgentDefinitionIdentifier:
      type: string
      format: uuid
      description: Stable Assign-owned identifier for a curated Agent definition. Definition identifiers are not resource UUIDv7 identifiers and clients must treat them as opaque.
    AgentTriggerType:
      type: string
      enum:
        - manual
        - event
        - schedule
    AgentDefinition:
      type: object
      additionalProperties: false
      required:
        - id
        - slug
        - name
        - category
        - description
        - responsibility
        - version
        - supported_trigger_types
        - default_trigger_types
        - capability_summary
        - action_boundary
      properties:
        id:
          $ref: "#/components/schemas/AgentDefinitionIdentifier"
        slug:
          type: string
          pattern: ^[a-z0-9]+(?:-[a-z0-9]+)*$
          minLength: 1
          maxLength: 80
        name:
          type: string
          minLength: 1
          maxLength: 120
        category:
          type: string
          minLength: 1
          maxLength: 80
        description:
          type: string
          minLength: 1
          maxLength: 500
        responsibility:
          type: string
          minLength: 1
          maxLength: 500
        version:
          type: string
          minLength: 1
          maxLength: 100
        supported_trigger_types:
          type: array
          minItems: 1
          maxItems: 3
          uniqueItems: true
          items:
            $ref: "#/components/schemas/AgentTriggerType"
        default_trigger_types:
          type: array
          minItems: 1
          maxItems: 3
          uniqueItems: true
          items:
            $ref: "#/components/schemas/AgentTriggerType"
        capability_summary:
          type: array
          minItems: 1
          maxItems: 20
          items:
            type: string
            minLength: 1
            maxLength: 200
        action_boundary:
          type: string
          minLength: 1
          maxLength: 500
    AgentConfiguration:
      type: object
      additionalProperties: false
      required:
        - status
        - scope_kind
        - project_ids
        - enabled_trigger_types
        - additional_instructions
      properties:
        status:
          type: string
          enum:
            - active
            - paused
        scope_kind:
          type: string
          enum:
            - workspace
            - projects
        project_ids:
          type: array
          maxItems: 1000
          uniqueItems: true
          items:
            $ref: "#/components/schemas/Identifier"
          description: Must be empty for Workspace scope and non-empty for selected-Projects scope.
        enabled_trigger_types:
          type: array
          minItems: 1
          maxItems: 3
          uniqueItems: true
          items:
            $ref: "#/components/schemas/AgentTriggerType"
        additional_instructions:
          type: string
          maxLength: 2000
        schedule_timezone:
          type: string
          minLength: 1
          maxLength: 100
          description: IANA timezone used for scheduled occurrences.
        schedule_local_time:
          type: string
          pattern: ^(?:[01][0-9]|2[0-3]):[0-5][0-9]$
          description: Minute-precision local wall time.
        schedule_weekdays:
          type: array
          minItems: 1
          maxItems: 7
          uniqueItems: true
          items:
            type: integer
            minimum: 1
            maximum: 7
          description: ISO weekdays where Monday is 1 and Sunday is 7.
        schedule_interval_minutes:
          oneOf:
            - type: integer
              const: 0
            - type: integer
              minimum: 60
              maximum: 10080
          default: 0
          description: Fixed recurrence interval. Zero uses the local-time weekday schedule.
    AgentInstanceCreateRequest:
      type: object
      additionalProperties: false
      required:
        - definition_id
        - status
        - scope_kind
        - project_ids
        - enabled_trigger_types
        - additional_instructions
      properties:
        definition_id:
          $ref: "#/components/schemas/AgentDefinitionIdentifier"
        status:
          type: string
          enum:
            - active
            - paused
        scope_kind:
          type: string
          enum:
            - workspace
            - projects
        project_ids:
          type: array
          maxItems: 1000
          uniqueItems: true
          items:
            $ref: "#/components/schemas/Identifier"
          description: Must be empty for Workspace scope and non-empty for selected-Projects scope.
        enabled_trigger_types:
          type: array
          minItems: 1
          maxItems: 3
          uniqueItems: true
          items:
            $ref: "#/components/schemas/AgentTriggerType"
        additional_instructions:
          type: string
          maxLength: 2000
        schedule_timezone:
          type: string
          minLength: 1
          maxLength: 100
        schedule_local_time:
          type: string
          pattern: ^(?:[01][0-9]|2[0-3]):[0-5][0-9]$
        schedule_weekdays:
          type: array
          minItems: 1
          maxItems: 7
          uniqueItems: true
          items:
            type: integer
            minimum: 1
            maximum: 7
        schedule_interval_minutes:
          oneOf:
            - type: integer
              const: 0
            - type: integer
              minimum: 60
              maximum: 10080
          default: 0
          description: Fixed recurrence interval. Zero uses the local-time weekday schedule.
    AgentInstance:
      type: object
      additionalProperties: false
      required:
        - id
        - workspace_id
        - definition
        - status
        - scope_kind
        - project_ids
        - enabled_trigger_types
        - additional_instructions
        - schedule_timezone
        - schedule_local_time
        - schedule_weekdays
        - schedule_interval_minutes
        - next_scheduled_at
        - quarantined_at
        - quarantine_reason
        - removed_at
        - rollback_definition_version
        - revision
        - created_at
        - updated_at
      properties:
        id:
          $ref: "#/components/schemas/Identifier"
        workspace_id:
          $ref: "#/components/schemas/Identifier"
        definition:
          $ref: "#/components/schemas/AgentDefinition"
        status:
          type: string
          enum:
            - active
            - paused
        scope_kind:
          type: string
          enum:
            - workspace
            - projects
        project_ids:
          type: array
          maxItems: 1000
          uniqueItems: true
          items:
            $ref: "#/components/schemas/Identifier"
          description: Empty for Workspace scope and non-empty for selected-Projects scope.
        enabled_trigger_types:
          type: array
          minItems: 1
          maxItems: 3
          uniqueItems: true
          items:
            $ref: "#/components/schemas/AgentTriggerType"
        additional_instructions:
          type: string
          maxLength: 2000
        schedule_timezone:
          type: string
          minLength: 1
          maxLength: 100
        schedule_local_time:
          type: string
          pattern: ^(?:[01][0-9]|2[0-3]):[0-5][0-9]$
        schedule_weekdays:
          type: array
          minItems: 1
          maxItems: 7
          uniqueItems: true
          items:
            type: integer
            minimum: 1
            maximum: 7
        schedule_interval_minutes:
          oneOf:
            - type: integer
              const: 0
            - type: integer
              minimum: 60
              maximum: 10080
          description: Fixed recurrence interval. Zero uses the local-time weekday schedule.
        next_scheduled_at:
          oneOf:
            - $ref: "#/components/schemas/Timestamp"
            - type: "null"
          description: Next occurrence as an unambiguous UTC instant; clients render it with schedule_timezone and the local offset.
        quarantined_at:
          oneOf:
            - $ref: "#/components/schemas/Timestamp"
            - type: "null"
        quarantine_reason:
          type: string
          maxLength: 200
        removed_at:
          oneOf:
            - $ref: "#/components/schemas/Timestamp"
            - type: "null"
        rollback_definition_version:
          type: string
          maxLength: 100
        revision:
          type: integer
          format: int64
          minimum: 1
        created_at:
          $ref: "#/components/schemas/Timestamp"
        updated_at:
          $ref: "#/components/schemas/Timestamp"
        catalog_updated_at:
          type: string
          pattern: ^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(?:[.][0-9]{1,9})?Z$
          description: UTC RFC3339 instance sort position on catalog list items; absent from detail and mutation responses.
    AgentDefinitionList:
      type: object
      additionalProperties: false
      required:
        - items
        - has_more
      properties:
        items:
          type: array
          maxItems: 100
          items:
            $ref: "#/components/schemas/AgentDefinition"
        next_cursor:
          oneOf:
            - $ref: "#/components/schemas/Cursor"
            - type: "null"
        has_more:
          type: boolean
    AgentInstanceList:
      type: object
      additionalProperties: false
      required:
        - items
        - has_more
      properties:
        items:
          type: array
          maxItems: 100
          items:
            $ref: "#/components/schemas/AgentInstance"
        next_cursor:
          oneOf:
            - $ref: "#/components/schemas/Cursor"
            - type: "null"
        has_more:
          type: boolean
    AgentRunCreateRequest:
      type: object
      additionalProperties: false
      maxProperties: 0
    AgentRunCancelRequest:
      type: object
      additionalProperties: false
      maxProperties: 0
    AgentQuarantineRequest:
      type: object
      additionalProperties: false
      required:
        - reason
      properties:
        reason:
          type: string
          minLength: 1
          maxLength: 200
    AgentRunState:
      type: string
      enum:
        - queued
        - running
        - waiting_for_approval
        - completed
        - no_action
        - failed
        - cancelled
        - billing_blocked
    AgentRun:
      type: object
      additionalProperties: false
      required:
        - id
        - workspace_id
        - agent_instance_id
        - definition_id
        - definition_version
        - trigger_type
        - state
        - initiated_by_actor_id
        - scheduled_for
        - safe_summary
        - error_code
        - reserved_credits
        - used_credits
        - cancellation_requested_at
        - started_at
        - finished_at
        - created_at
        - updated_at
      properties:
        id:
          $ref: "#/components/schemas/Identifier"
        workspace_id:
          $ref: "#/components/schemas/Identifier"
        agent_instance_id:
          $ref: "#/components/schemas/Identifier"
        definition_id:
          $ref: "#/components/schemas/AgentDefinitionIdentifier"
        definition_version:
          type: string
          minLength: 1
          maxLength: 100
        trigger_type:
          $ref: "#/components/schemas/AgentTriggerType"
        state:
          $ref: "#/components/schemas/AgentRunState"
        initiated_by_actor_id:
          $ref: "#/components/schemas/Identifier"
        scheduled_for:
          oneOf:
            - $ref: "#/components/schemas/Timestamp"
            - type: "null"
        safe_summary:
          type: string
          maxLength: 4000
        error_code:
          description: Safe failure code. agent_resolution_exhausted indicates an unstarted dispatch exhausted resolution attempts and was reconciled; it does not imply an action occurred.
          oneOf:
            - type: string
              minLength: 1
              maxLength: 100
            - type: "null"
        reserved_credits:
          type: integer
          format: int64
          minimum: 0
        used_credits:
          type: integer
          format: int64
          minimum: 0
        cancellation_requested_at:
          oneOf:
            - $ref: "#/components/schemas/Timestamp"
            - type: "null"
        started_at:
          oneOf:
            - $ref: "#/components/schemas/Timestamp"
            - type: "null"
        finished_at:
          oneOf:
            - $ref: "#/components/schemas/Timestamp"
            - type: "null"
        created_at:
          $ref: "#/components/schemas/Timestamp"
        updated_at:
          $ref: "#/components/schemas/Timestamp"
    AgentRunList:
      type: object
      additionalProperties: false
      required:
        - items
        - has_more
      properties:
        total_count:
          type: integer
          format: int64
          minimum: 0
          description: Authorized collection size before cursor bounds; refreshed at read time. Activity counts admitted events before display grouping and Comments include retained ordinals.
        items:
          type: array
          maxItems: 50
          items:
            $ref: "#/components/schemas/AgentRun"
        next_cursor:
          oneOf:
            - $ref: "#/components/schemas/Cursor"
            - type: "null"
        has_more:
          type: boolean
    AgentApproval:
      type: object
      additionalProperties: false
      required:
        - id
        - state
        - expires_at
        - decided_by_actor_id
        - decided_at
        - decision_reason
      properties:
        id:
          $ref: "#/components/schemas/Identifier"
        state:
          type: string
          enum:
            - pending
            - approved
            - rejected
            - expired
            - invalidated
        expires_at:
          $ref: "#/components/schemas/Timestamp"
        decided_by_actor_id:
          type: string
        decided_at:
          oneOf:
            - $ref: "#/components/schemas/Timestamp"
            - type: "null"
        decision_reason:
          type: string
          maxLength: 500
    AgentAction:
      type: object
      additionalProperties: false
      required:
        - id
        - run_id
        - tool_name
        - tool_contract_version
        - capability
        - target_type
        - target_id
        - request_digest
        - preview
        - idempotency_key
        - state
        - result_target_revision
        - created_at
      properties:
        id:
          $ref: "#/components/schemas/Identifier"
        run_id:
          $ref: "#/components/schemas/Identifier"
        tool_name:
          type: string
          maxLength: 120
        tool_contract_version:
          type: string
          maxLength: 120
        capability:
          type: string
          enum:
            - observe
            - communicate
            - manage
        target_type:
          type: string
          maxLength: 80
        target_id:
          type: string
        request_digest:
          type: string
          pattern: ^sha256:[0-9a-f]{64}$
        preview:
          type: string
          maxLength: 2000
        proposal:
          type: object
          additionalProperties: true
          description: Exact immutable proposal reviewed by the approver.
        idempotency_key:
          type: string
          maxLength: 200
        state:
          type: string
          enum:
            - completed
            - pending_approval
            - rejected
            - expired
            - invalidated
            - denied
            - failed
        result_target_revision:
          oneOf:
            - type: integer
              format: int64
            - type: "null"
        approval:
          $ref: "#/components/schemas/AgentApproval"
        created_at:
          $ref: "#/components/schemas/Timestamp"
    AgentActionList:
      type: object
      additionalProperties: false
      required:
        - items
        - has_more
      properties:
        items:
          type: array
          maxItems: 100
          items:
            $ref: "#/components/schemas/AgentAction"
        next_cursor:
          oneOf:
            - $ref: "#/components/schemas/Cursor"
            - type: "null"
        has_more:
          type: boolean
    AgentApprovalDecision:
      type: object
      additionalProperties: false
      required:
        - decision
        - reason
      properties:
        decision:
          type: string
          enum:
            - approved
            - rejected
        reason:
          type: string
          maxLength: 500
    AgentOutput:
      type: object
      additionalProperties: false
      required:
        - id
        - actor_id
        - run_id
        - agent_instance_id
        - definition_version
        - kind
        - target_type
        - target_id
        - content
        - created_at
      properties:
        id:
          $ref: "#/components/schemas/Identifier"
        actor_id:
          $ref: "#/components/schemas/Identifier"
        run_id:
          $ref: "#/components/schemas/Identifier"
        agent_instance_id:
          $ref: "#/components/schemas/Identifier"
        definition_version:
          type: string
          minLength: 1
          maxLength: 100
        kind:
          type: string
          enum:
            - comment
            - report
            - notification
        target_type:
          type: string
          maxLength: 80
        target_id:
          type: string
        content:
          type: string
          minLength: 1
          maxLength: 4000
        created_at:
          $ref: "#/components/schemas/Timestamp"
    AgentOutputList:
      type: object
      additionalProperties: false
      required:
        - items
        - has_more
      properties:
        items:
          type: array
          maxItems: 100
          items:
            $ref: "#/components/schemas/AgentOutput"
        next_cursor:
          oneOf:
            - $ref: "#/components/schemas/Cursor"
            - type: "null"
        has_more:
          type: boolean
    CodeSuggestionReference:
      type: object
      additionalProperties: false
      required:
        - repository_id
        - repository_name
        - path
        - revision
      properties:
        repository_id:
          $ref: "#/components/schemas/Identifier"
        repository_name:
          type: string
          maxLength: 512
        path:
          type: string
          maxLength: 2000
        revision:
          type: string
          pattern: ^[0-9a-f]{40}$
        symbol:
          oneOf:
            - type: string
              maxLength: 1000
            - type: "null"
        line:
          oneOf:
            - type: integer
              minimum: 1
            - type: "null"
    RelatedContextItem:
      type: object
      additionalProperties: false
      description: A discovered Task, Document, verified GitHub commit or read-only indexed code reference the Task is not linked to yet, or one the person already confirmed as a Suggestion. Commit entity IDs identify Core-owned comment findings. Manually linked resources are never suggested.
      required:
        - kind
        - entity_id
        - project_id
        - identifier
        - project_path
        - title
        - why
        - excerpt
        - source_time
        - confidence
        - confirmed
      properties:
        code_reference:
          $ref: "#/components/schemas/CodeSuggestionReference"
        kind:
          type: string
          enum:
            - task
            - document
            - commit
            - code
        entity_id:
          oneOf:
            - $ref: "#/components/schemas/Identifier"
            - type: "null"
        project_id:
          oneOf:
            - $ref: "#/components/schemas/Identifier"
            - type: "null"
        identifier:
          oneOf:
            - type: string
              maxLength: 500
            - type: "null"
          description: Current Core-owned Task key or Document path; a verified HTTPS GitHub commit URL for commit findings.
        project_path:
          oneOf:
            - type: string
              maxLength: 100
            - type: "null"
        title:
          type: string
          maxLength: 1000
        why:
          type: string
          maxLength: 2000
          description: Template reason for the suggestion.
        excerpt:
          type: string
          maxLength: 1000
          description: Supporting passage from the suggested source.
        source_time:
          type: string
          format: date-time
        confidence:
          type: string
          enum:
            - high
            - medium
            - low
        confirmed:
          type: boolean
          description: True when the person already confirmed this finding for the Task. It is a durable link now, listed after the pending findings so the outcome stays visible, and can no longer be confirmed or discarded.
    SuggestionFeedbackRequest:
      type: object
      additionalProperties: false
      required:
        - entity_kind
        - entity_id
        - outcome
      properties:
        entity_kind:
          type: string
          enum:
            - task
            - document
            - commit
        entity_id:
          $ref: "#/components/schemas/Identifier"
        outcome:
          type: string
          enum:
            - confirmed
            - discarded
    SuggestionFeedbackResponse:
      type: object
      additionalProperties: false
      required:
        - accepted
      properties:
        accepted:
          type: boolean
    TaskRelatedContext:
      type: object
      additionalProperties: false
      required:
        - state
        - results
        - stale
        - observed_at
        - truncated
      properties:
        state:
          type: string
          enum:
            - ready
            - empty
            - unavailable
        results:
          type: array
          maxItems: 25
          description: At most five pending findings followed by at most twenty confirmed ones.
          items:
            $ref: "#/components/schemas/RelatedContextItem"
        stale:
          type: boolean
        observed_at:
          oneOf:
            - type: string
              format: date-time
            - type: "null"
        truncated:
          type: boolean
    KnowledgeSearchItem:
      type: object
      additionalProperties: false
      required:
        - kind
        - entity_id
        - project_id
        - title
        - excerpt
        - why
        - provenance
        - source_time
        - identifier
        - project_path
        - task_id
        - repository_id
        - repository_path
        - symbol
      properties:
        kind:
          type: string
          minLength: 1
          maxLength: 80
        entity_id:
          oneOf:
            - $ref: "#/components/schemas/Identifier"
            - type: "null"
        project_id:
          oneOf:
            - $ref: "#/components/schemas/Identifier"
            - type: "null"
        title:
          type: string
          maxLength: 1000
        excerpt:
          type: string
          maxLength: 4000
        why:
          type: string
          maxLength: 2000
        provenance:
          type: string
          enum:
            - authoritative
            - extracted
            - inferred
        source_time:
          type: string
          format: date-time
        identifier:
          oneOf:
            - type: string
              maxLength: 500
            - type: "null"
          description: Current Core-owned canonical resource identifier when resolvable.
        project_path:
          oneOf:
            - type: string
              maxLength: 100
            - type: "null"
        task_id:
          oneOf:
            - $ref: "#/components/schemas/Identifier"
            - type: "null"
          description: Parent Task for a Comment result when resolvable.
        repository_id:
          oneOf:
            - $ref: "#/components/schemas/Identifier"
            - type: "null"
        repository_path:
          oneOf:
            - type: string
              maxLength: 4096
            - type: "null"
        symbol:
          oneOf:
            - type: string
              maxLength: 1000
            - type: "null"
    KnowledgeSearchProjection:
      type: object
      additionalProperties: false
      required:
        - state
        - results
        - stale
        - observed_at
        - truncated
      properties:
        state:
          type: string
          enum:
            - ready
            - empty
            - unavailable
        results:
          type: array
          maxItems: 20
          items:
            $ref: "#/components/schemas/KnowledgeSearchItem"
        stale:
          type: boolean
        observed_at:
          oneOf:
            - type: string
              format: date-time
            - type: "null"
        truncated:
          type: boolean
    KnowledgeAnswer:
      type: object
      additionalProperties: false
      required:
        - status
        - answer
        - sources
        - stale
        - truncated
        - synthesis
      properties:
        status:
          type: string
          enum:
            - supported
            - partial
            - conflicting
            - none
        answer:
          type: string
          maxLength: 4000
          description: Bounded extractive answer or an abstention.
        sources:
          type: array
          maxItems: 5
          items:
            $ref: "#/components/schemas/KnowledgeSearchItem"
        stale:
          type: boolean
        truncated:
          type: boolean
        synthesis:
          type: string
          enum:
            - extractive
    DiscussEventPage:
      type: object
      additionalProperties: false
      required:
        - stream_id
        - events
        - cursor
        - has_more
      properties:
        stream_id:
          $ref: "#/components/schemas/Identifier"
        cursor:
          type: integer
          format: int64
          minimum: 0
        has_more:
          type: boolean
        events:
          type: array
          maxItems: 100
          items:
            type: object
            additionalProperties: false
            required:
              - sequence
              - message
            properties:
              sequence:
                type: integer
                format: int64
                minimum: 1
              message:
                $ref: "#/components/schemas/DiscussMessage"
    DiscussRunEventPage:
      type: object
      additionalProperties: false
      required:
        - run_id
        - run_state
        - events
        - cursor
        - has_more
      properties:
        run_id:
          $ref: "#/components/schemas/Identifier"
        run_state:
          type: string
          enum:
            - queued
            - running
            - cancel_requested
            - completed
            - failed
            - cancelled
        cursor:
          type: integer
          format: int64
          minimum: 0
        has_more:
          type: boolean
        events:
          type: array
          maxItems: 100
          items:
            $ref: "#/components/schemas/DiscussRunEvent"
    DiscussRunEvent:
      type: object
      additionalProperties: false
      required:
        - sequence
        - event_id
        - kind
        - state
        - summary
        - details_available
        - created_at
      properties:
        sequence:
          type: integer
          format: int64
          minimum: 1
        event_id:
          $ref: "#/components/schemas/Identifier"
        tool_call_id:
          type: string
          format: uuid
        kind:
          type: string
          enum:
            - run.snapshot
            - run.queued
            - run.started
            - run.cancel_requested
            - run.completed
            - run.failed
            - run.cancelled
            - tool.started
            - tool.completed
            - tool.failed
            - tool.cancelled
            - status.changed
            - run.terminal
            - interaction.required
            - interaction.resolved
            - result.available
        state:
          type: string
          enum:
            - queued
            - running
            - cancel_requested
            - completed
            - failed
            - cancelled
        summary:
          type: string
          maxLength: 1000
        details_available:
          type: boolean
        detail:
          type: object
          additionalProperties: false
          properties:
            public_name:
              type: string
              minLength: 2
              maxLength: 100
            operation:
              type: string
              enum:
                - read
                - write
                - delegate
            state:
              type: string
              enum:
                - running
                - completed
                - failed
                - cancelled
            duration_ms:
              type: integer
              format: int64
              minimum: 0
            result_count:
              type: integer
              minimum: 0
        created_at:
          type: string
          format: date-time
    DiscussSpecialistRunPage:
      type: object
      additionalProperties: false
      required:
        - specialist_runs
      properties:
        specialist_runs:
          type: array
          maxItems: 50
          items:
            $ref: "#/components/schemas/DiscussSpecialistRun"
    DiscussSpecialistRun:
      type: object
      additionalProperties: false
      required:
        - id
        - parent_run_id
        - source_run_id
        - specialist_name
        - state
        - safe_summary
        - reserved_credits
        - used_credits
        - created_at
        - updated_at
      properties:
        id:
          $ref: "#/components/schemas/Identifier"
        parent_run_id:
          $ref: "#/components/schemas/Identifier"
        source_run_id:
          $ref: "#/components/schemas/Identifier"
        specialist_name:
          type: string
          minLength: 1
          maxLength: 100
        state:
          type: string
          enum:
            - queued
            - running
            - input_required
            - reconciliation_required
            - completed
            - failed
            - cancelled
            - expired
        safe_summary:
          type: string
          maxLength: 4000
        error_code:
          type: string
          maxLength: 100
        reserved_credits:
          type: integer
          format: int64
          minimum: 0
        used_credits:
          type: integer
          format: int64
          minimum: 0
        interaction:
          type: object
          additionalProperties: false
          required:
            - id
            - kind
            - prompt
            - expires_at
          properties:
            id:
              $ref: "#/components/schemas/Identifier"
            kind:
              type: string
              enum:
                - clarification
                - approval
            prompt:
              type: object
              description: Bounded safe prompt projected by the canonical work-session journal.
            expires_at:
              type: string
              format: date-time
        completion_message_id:
          $ref: "#/components/schemas/Identifier"
        cancellation_requested_at:
          type: string
          format: date-time
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    DiscussSpecialistResumeRequest:
      type: object
      additionalProperties: false
      required:
        - interruptId
        - status
        - payload
      properties:
        interruptId:
          $ref: "#/components/schemas/Identifier"
        status:
          type: string
          enum:
            - resolved
        payload:
          type: object
          additionalProperties: false
          required:
            - answer
          properties:
            answer:
              type: string
              minLength: 1
              maxLength: 2000
    DiscussAvailability:
      type: object
      additionalProperties: false
      required:
        - feature
        - access
        - reason
        - history_present
        - can_manage_billing
        - minimum_plan_key
        - minimum_plan_label
      properties:
        feature:
          type: string
          const: discuss
        access:
          type: string
          enum:
            - full
            - read_only
            - preview
        reason:
          type: string
          enum:
            - entitled
            - plan_required
            - entitlement_ended
            - credits_exhausted
            - billing_restricted
        history_present:
          type: boolean
        can_manage_billing:
          type: boolean
        minimum_plan_key:
          type: string
          const: personal
        minimum_plan_label:
          type: string
          minLength: 1
          maxLength: 80
    DiscussLiveSessionRequest:
      type: object
      additionalProperties: false
      required:
        - sdp
      properties:
        sdp:
          type: string
          minLength: 1
          maxLength: 65536
          description: Browser WebRTC SDP offer after ICE gathering completes.
        locale:
          type: string
          minLength: 2
          maxLength: 35
          pattern: ^[A-Za-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$
          description: Optional BCP 47 locale hint.
        spoken_response_mode:
          type: string
          enum:
            - off
            - confirmations
            - full
          default: full
          description: Account playback policy for generated voice output.
        confirmation_level:
          type: string
          enum:
            - standard
            - confirm_more
          default: standard
          description: Account safety preference for how readily Voice asks before actions.
    DiscussLiveSession:
      type: object
      additionalProperties: false
      required:
        - session_id
        - sdp
        - max_duration_seconds
      properties:
        session_id:
          type: string
          minLength: 1
          maxLength: 200
        sdp:
          type: string
          minLength: 1
          maxLength: 65536
          description: Provider WebRTC SDP answer.
        max_duration_seconds:
          type: integer
          minimum: 15
          maximum: 600
          description: Upper session lifetime enforced by client teardown and server authority checks.
        context_cursor:
          type: integer
          format: int64
          minimum: 0
          description: Canonical Discuss event cursor captured with startup history. Catch up subsequent authorized events from this cursor before declaring context ready; never replay them as new user input.
    DiscussMessage:
      type: object
      additionalProperties: false
      required:
        - id
        - sequence
        - role
        - kind
        - state
        - content
        - parts
        - correlation_id
        - created_at
        - finalized_at
      properties:
        revision:
          type: integer
          format: int64
          minimum: 1
          description: Monotonic message revision for deduplication.
        parent_message_id:
          type:
            - string
            - "null"
          format: uuid
        id:
          $ref: "#/components/schemas/Identifier"
        sequence:
          type: integer
          format: int64
          minimum: 1
        role:
          type: string
          enum:
            - user
            - assistant
            - agent
            - system
        kind:
          type: string
          enum:
            - message
            - knowledge_answer
            - finding
            - reminder
            - ceremony
            - result
        state:
          type: string
          enum:
            - pending
            - final
            - failed
            - cancelled
        run_state:
          type: string
          enum:
            - queued
            - running
            - cancel_requested
            - completed
            - failed
            - cancelled
          description: Current server-owned generation state when this message has a durable run. Omitted for messages without a run.
        content:
          type: string
          maxLength: 16000
        parts:
          type: array
          maxItems: 20
          description: Extensible typed message parts. specialist_result may include delivery_mode (direct or supporting) and an author object with actor_id, definition_id, version, revision, name, initiator_actor_id and parent_run_id pinned at admission. Direct results retain that author; supporting results are presented as Discuss. Unknown or absent attribution uses the generic Agent label. runtime_text is a full pending-text snapshot with stable part_id, optional run_id/step_id/generation/revision, producer_fence, producer_sequence and a UTF-8 byte offset; snapshots replace text and never imply concatenation or provider usage. context_capsule records Core-resolved visible UI scope. tool_activity is a stable Core-sanitized user projection keyed by tool_call_id, with optional part_id/run_id/step_id/revision/producer_fence for independently versioned actual handler activity. States describe recorded execution, never effect confirmation; it excludes raw arguments/results, prompts, credentials and hidden reasoning. entity_references contains at most 20 Core-reauthorized Project/Task identities with canonical labels/locators and observed revisions for trusted client navigation. task_result identifies one exact read or committed single-Task create/update with task_id, task_code, title, observed_revision and reason; clients must hydrate current authorized Task properties rather than treating it as a frozen snapshot. Completed Discuss mutations include an immutable action_result with action/receipt IDs, before/after state, entity version, actor, timestamp, risk level and a conditional inverse when reversible. interaction represents approval, ambiguity, stale-action and recovery states; policy-required reviews include the exact ChangeSet ID, version and digest. operation_receipts groups bounded canonical receipt references in a receipts array with operation_id, tool, state and optional resource_type, resource_id and revision. Receipts and mutations commit together; model prose alone is not a receipt.
          items:
            type: object
        correlation_id:
          $ref: "#/components/schemas/Identifier"
        created_at:
          type: string
          format: date-time
        finalized_at:
          oneOf:
            - type: string
              format: date-time
            - type: "null"
    DiscussMessagePage:
      type: object
      additionalProperties: false
      required:
        - stream_id
        - messages
        - next_before
        - unread_count
        - last_sequence
        - last_read_sequence
      properties:
        event_cursor:
          type: integer
          format: int64
          minimum: 0
          description: Replay cursor captured atomically with this history page.
        stream_id:
          $ref: "#/components/schemas/Identifier"
        messages:
          type: array
          maxItems: 100
          items:
            $ref: "#/components/schemas/DiscussMessage"
        next_before:
          oneOf:
            - type: integer
              format: int64
              minimum: 1
            - type: "null"
        unread_count:
          type: integer
          format: int64
          minimum: 0
        last_sequence:
          type: integer
          format: int64
          minimum: 0
        last_read_sequence:
          type: integer
          format: int64
          minimum: 0
    DiscussNativeReference:
      type: object
      additionalProperties: false
      required:
        - kind
        - id
      properties:
        kind:
          type: string
          enum:
            - user
            - task
            - document
            - project
            - agent
        id:
          type: string
          minLength: 1
          maxLength: 200
          description: Canonical ID or a Workspace-local editor link locator.
    DiscussResolvedReference:
      type: object
      additionalProperties: false
      required:
        - kind
        - id
        - label
        - locator
        - hint
      properties:
        kind:
          type: string
        id:
          $ref: "#/components/schemas/Identifier"
        label:
          type: string
        locator:
          type: string
        hint:
          type: string
    DiscussFile:
      type: object
      additionalProperties: false
      required:
        - id
        - filename
        - content_length
        - content_type
        - scan_state
        - created_at
      properties:
        id:
          $ref: "#/components/schemas/Identifier"
        filename:
          type: string
        content_length:
          type: integer
          format: int64
        content_type:
          type: string
        scan_state:
          type: string
          description: Only clean files can be submitted or downloaded.
        created_at:
          $ref: "#/components/schemas/Timestamp"
    DiscussUploadRequest:
      type: object
      additionalProperties: false
      required:
        - filename
        - content_length
        - sha256
      properties:
        filename:
          type: string
          minLength: 1
          maxLength: 255
        content_length:
          type: integer
          format: int64
          minimum: 1
          maximum: 10485760
        sha256:
          type: string
          pattern: ^[a-f0-9]{64}$
    DiscussFileURL:
      type: object
      additionalProperties: false
      required:
        - url
        - expires_at
      properties:
        url:
          type: string
          format: uri
        expires_at:
          $ref: "#/components/schemas/Timestamp"
    DiscussEnqueueRequest:
      type: object
      additionalProperties: false
      required:
        - content
      properties:
        content:
          type: string
          maxLength: 8000
        parts:
          type: array
          maxItems: 4
          description: "Optional per-turn metadata only: task_selection with selection_id (Core freezes result_set_id/version), turn_preferences with locale and IANA timezone, evidence_collection with exact id/version, or search_result with query and result_version from the first 20 unfiltered work results. At most one of each. Evidence collection pins require current private ownership and the latest version; stale pins return revision_conflict before admission. Identical acknowledged retries recover the historical handle without granting current source authority. Core repeats and reauthorizes that lexical query before Send; a changed version returns revision_conflict without admitting a turn. Search metadata grants no action authority or exhaustive coverage. Other part types or fields are rejected."
          items:
            type: object
        entity_references:
          type: array
          maxItems: 5
          items:
            $ref: "#/components/schemas/DiscussNativeReference"
        attachment_ids:
          type: array
          maxItems: 5
          uniqueItems: true
          description: Clean files in the caller's private stream; at most 20 MiB combined. Empty content requires at least one file.
          items:
            $ref: "#/components/schemas/Identifier"
        context:
          $ref: "#/components/schemas/DiscussContextInput"
    DiscussContextInput:
      type: object
      additionalProperties: false
      required:
        - surface
      properties:
        surface:
          type: string
          enum:
            - global
            - project
            - task
            - document
            - agent
            - selection
            - text_selection
            - activity
        project:
          $ref: "#/components/schemas/DiscussNativeReference"
        active_entity:
          $ref: "#/components/schemas/DiscussNativeReference"
        selected_entities:
          type: array
          maxItems: 20
          uniqueItems: true
          items:
            $ref: "#/components/schemas/DiscussNativeReference"
    DiscussSendRequest:
      type: object
      additionalProperties: false
      required:
        - content
      properties:
        content:
          type: string
          minLength: 1
          maxLength: 8000
        parts:
          type: array
          maxItems: 20
          items:
            type: object
        context:
          $ref: "#/components/schemas/DiscussContextInput"
    DiscussTurn:
      type: object
      additionalProperties: false
      required:
        - stream_id
        - user
        - response
        - replayed
      properties:
        stream_id:
          $ref: "#/components/schemas/Identifier"
        user:
          $ref: "#/components/schemas/DiscussMessage"
        response:
          $ref: "#/components/schemas/DiscussMessage"
        replayed:
          type: boolean
    DiscussInteractionOption:
      type: object
      additionalProperties: false
      required:
        - id
        - label
      properties:
        id:
          type: string
          minLength: 1
          maxLength: 100
        label:
          type: string
          minLength: 1
          maxLength: 200
    DiscussInteraction:
      type: object
      additionalProperties: false
      required:
        - id
        - message_id
        - kind
        - state
        - revision
        - prompt
        - options
        - allow_free_text
        - decision
        - subject_message_revision
        - policy_version
        - tool_manifest_version
        - expires_at
        - decided_by
        - decided_at
      properties:
        id:
          $ref: "#/components/schemas/Identifier"
        message_id:
          $ref: "#/components/schemas/Identifier"
        kind:
          type: string
          enum:
            - clarification
            - approval
        state:
          type: string
          enum:
            - open
            - resolved
            - rejected
            - stale
            - expired
            - superseded
        revision:
          type: integer
          format: int64
          minimum: 1
        prompt:
          type: string
          minLength: 1
          maxLength: 1600
        options:
          type: array
          maxItems: 7
          items:
            $ref: "#/components/schemas/DiscussInteractionOption"
        allow_free_text:
          type: boolean
        decision:
          oneOf:
            - type: string
              maxLength: 4000
            - type: "null"
        subject_message_revision:
          type: integer
          format: int64
          minimum: 1
        policy_version:
          type: string
          minLength: 1
          maxLength: 100
        tool_manifest_version:
          type: string
          minLength: 1
          maxLength: 100
        expires_at:
          type: string
          format: date-time
        decided_by:
          oneOf:
            - $ref: "#/components/schemas/Identifier"
            - type: "null"
        decided_at:
          oneOf:
            - type: string
              format: date-time
            - type: "null"
        intent_id:
          readOnly: true
          description: Read-only Core-owned durable intent this wait belongs to. Absent or null for interactions created before the intent contract; clients never write it.
          oneOf:
            - $ref: "#/components/schemas/Identifier"
            - type: "null"
        intent_revision:
          readOnly: true
          description: Intent revision the decision was applied under. Read-only.
          oneOf:
            - type: integer
              format: int64
              minimum: 1
            - type: "null"
        intent_state:
          readOnly: true
          description: Read-only public intent lifecycle after this decision; null when not read.
          oneOf:
            - type: string
              enum:
                - ready
                - waiting
                - executing
                - suspended
                - superseded
                - cancelled
                - completed
                - failed
            - type: "null"
    DiscussInteractionDecisionRequest:
      type: object
      additionalProperties: false
      required:
        - expected_revision
        - decision
      properties:
        proposal:
          description: Explicitly review this exact current version of the same ChangeSet bound to the approval. Core atomically updates the binding and admits the continuation; editing alone never approves. Omit for existing unchanged approvals and clarifications.
          $ref: "#/components/schemas/DiscussInteractionProposal"
        expected_revision:
          type: integer
          format: int64
          minimum: 1
        decision:
          type: string
          minLength: 1
          maxLength: 4000
    DiscussInteractionProposal:
      type: object
      additionalProperties: false
      required:
        - id
        - version
        - digest
      properties:
        id:
          $ref: "#/components/schemas/Identifier"
        version:
          type: integer
          format: int64
          minimum: 1
        digest:
          type: string
          minLength: 1
          maxLength: 100
    DiscussInteractionTurn:
      type: object
      additionalProperties: false
      required:
        - interaction
        - turn
      properties:
        interaction:
          $ref: "#/components/schemas/DiscussInteraction"
        turn:
          $ref: "#/components/schemas/DiscussTurn"
    DiscussReadRequest:
      type: object
      additionalProperties: false
      required:
        - through_sequence
      properties:
        through_sequence:
          type: integer
          format: int64
          minimum: 0
    DiscussReadState:
      type: object
      additionalProperties: false
      required:
        - stream_id
        - last_read_sequence
        - revision
      properties:
        stream_id:
          $ref: "#/components/schemas/Identifier"
        last_read_sequence:
          type: integer
          format: int64
          minimum: 0
        revision:
          type: integer
          format: int64
          minimum: 1
    DiscussPreferencesUpdate:
      type: object
      required:
        - timezone
        - workday_start
        - workday_end
        - workweek
        - start_day_enabled
        - end_day_enabled
        - end_week_enabled
        - push_enabled
        - personality
        - revision
      properties:
        timezone:
          type: string
          minLength: 1
          maxLength: 100
          description: IANA timezone used only by this Workspace membership's Discuss schedule.
        timezone_inherited:
          type: boolean
          description: When true, timezone tracks the Account timezone and is returned as the resolved effective value.
        workday_start:
          type: string
          pattern: ^([01][0-9]|2[0-3]):[0-5][0-9]$
        workday_end:
          type: string
          pattern: ^([01][0-9]|2[0-3]):[0-5][0-9]$
        workweek:
          type: array
          minItems: 1
          maxItems: 7
          uniqueItems: true
          items:
            type: integer
            minimum: 1
            maximum: 7
        start_day_enabled:
          type: boolean
        end_day_enabled:
          type: boolean
        end_week_enabled:
          type: boolean
        push_enabled:
          type: boolean
          description: Conservative master opt-in; existing notification/device policies still apply.
        personality:
          type: string
          enum:
            - concise
            - friendly
            - professional
            - grumpy
        revision:
          type: integer
          format: int64
          minimum: 1
    DiscussPreferences:
      allOf:
        - $ref: "#/components/schemas/DiscussPreferencesUpdate"
        - type: object
          required:
            - updated_at
          properties:
            updated_at:
              type: string
              format: date-time
    DiscussReminderCreate:
      type: object
      additionalProperties: false
      required:
        - title
        - due_at
        - timezone
      properties:
        title:
          type: string
          minLength: 1
          maxLength: 500
        note:
          type: string
          maxLength: 4000
          default: ""
        subject:
          oneOf:
            - type: object
            - type: "null"
        due_at:
          type: string
          format: date-time
        timezone:
          type: string
          minLength: 1
          maxLength: 100
    DiscussReminder:
      type: object
      additionalProperties: false
      required:
        - id
        - title
        - note
        - subject
        - due_at
        - timezone
        - state
        - occurrence
        - revision
        - created_at
        - updated_at
      properties:
        id:
          $ref: "#/components/schemas/Identifier"
        title:
          type: string
          minLength: 1
          maxLength: 500
        note:
          type: string
          maxLength: 4000
        subject:
          oneOf:
            - type: object
            - type: "null"
        due_at:
          type: string
          format: date-time
        timezone:
          type: string
          minLength: 1
          maxLength: 100
        state:
          type: string
          enum:
            - scheduled
            - delivered
            - completed
            - snoozed
            - cancelled
        occurrence:
          type: integer
          minimum: 1
        revision:
          type: integer
          format: int64
          minimum: 1
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    DiscussReminderList:
      type: object
      additionalProperties: false
      required:
        - items
      properties:
        items:
          type: array
          maxItems: 100
          items:
            $ref: "#/components/schemas/DiscussReminder"
    DiscussReminderStateUpdate:
      type: object
      additionalProperties: false
      required:
        - state
        - revision
      properties:
        state:
          type: string
          enum:
            - scheduled
            - completed
            - snoozed
            - cancelled
        due_at:
          oneOf:
            - type: string
              format: date-time
            - type: "null"
          description: Required for snooze and ignored otherwise.
        revision:
          type: integer
          format: int64
          minimum: 1
    BillingInvoiceReference:
      type: object
      additionalProperties: false
      required:
        - id
        - status
        - currency
        - amount_due
        - issued_at
        - due_at
      properties:
        id:
          $ref: "#/components/schemas/Identifier"
        status:
          type: string
          maxLength: 40
        currency:
          oneOf:
            - type: string
              minLength: 3
              maxLength: 3
            - type: "null"
        amount_due:
          oneOf:
            - type: integer
              format: int64
              minimum: 0
            - type: "null"
        issued_at:
          oneOf:
            - type: string
              format: date-time
            - type: "null"
        due_at:
          oneOf:
            - type: string
              format: date-time
            - type: "null"
    BillingSettings:
      type: object
      additionalProperties: false
      required:
        - plan_key
        - state
        - billing_cadence
        - current_period_ends_at
        - cancel_at_period_end
        - provider
        - has_provider_customer
        - invoices
      properties:
        plan_key:
          type: string
          maxLength: 100
        state:
          type: string
          maxLength: 40
        billing_cadence:
          oneOf:
            - type: string
              maxLength: 40
            - type: "null"
        current_period_ends_at:
          oneOf:
            - type: string
              format: date-time
            - type: "null"
        cancel_at_period_end:
          type: boolean
        provider:
          oneOf:
            - type: string
              maxLength: 100
            - type: "null"
        has_provider_customer:
          type: boolean
        invoices:
          type: array
          maxItems: 20
          items:
            $ref: "#/components/schemas/BillingInvoiceReference"
    BillingSettingsResponse:
      type: object
      additionalProperties: false
      required:
        - subscription
        - knowledge_credits
        - agent_entitlement
        - provider_available
        - provider_unavailable_reason
        - sandbox_credit_topups_available
      properties:
        subscription:
          $ref: "#/components/schemas/BillingSettings"
        knowledge_credits:
          oneOf:
            - $ref: "#/components/schemas/KnowledgeCreditSummary"
            - type: "null"
        agent_entitlement:
          $ref: "#/components/schemas/AgentEntitlementSummary"
        provider_available:
          type: boolean
        sandbox_credit_topups_available:
          type: boolean
          description: Compatibility field indicating whether verified provider-hosted AI Credit checkout is available in this environment.
        provider_unavailable_reason:
          type: string
          maxLength: 500
    AgentEntitlementSummary:
      type: object
      additionalProperties: false
      required:
        - key
        - enabled
        - active_limit
        - active_used
        - retained_limit
        - retained_used
      properties:
        key:
          type: string
          const: agents.canonical
        enabled:
          type: boolean
        active_limit:
          type: integer
          format: int64
          minimum: 0
          maximum: 5
        active_used:
          type: integer
          format: int64
          minimum: 0
        retained_limit:
          type: integer
          format: int64
          const: 100
        retained_used:
          type: integer
          format: int64
          minimum: 0
    KnowledgeCreditSummary:
      type: object
      additionalProperties: false
      required:
        - workspace_id
        - subscription_credits
        - promotional_credits
        - purchased_credits
        - reserved_credits
        - available_credits
        - current_period_usage
        - observed_at
      properties:
        workspace_id:
          $ref: "#/components/schemas/Identifier"
        subscription_credits:
          type: integer
          format: int64
        promotional_credits:
          type: integer
          format: int64
        purchased_credits:
          type: integer
          format: int64
        reserved_credits:
          type: integer
          format: int64
          minimum: 0
        available_credits:
          type: integer
          format: int64
        renewal_at:
          oneOf:
            - type: string
              format: date-time
            - type: "null"
        current_period_usage:
          type: integer
          format: int64
          minimum: 0
        pricing_version:
          oneOf:
            - type: string
              maxLength: 80
            - type: "null"
        observed_at:
          type: string
          format: date-time
    AccountBillingWorkspace:
      type: object
      additionalProperties: false
      required:
        - workspace_id
        - name
        - slug
        - relationship
        - plan_key
        - state
        - can_manage_billing
      properties:
        workspace_id:
          $ref: "#/components/schemas/Identifier"
        name:
          type: string
          minLength: 1
          maxLength: 100
        slug:
          type: string
          minLength: 1
          maxLength: 63
        relationship:
          type: string
          enum:
            - owner
            - admin
            - member
            - viewer
        plan_key:
          type: string
          maxLength: 100
        state:
          type: string
          enum:
            - free
            - active
            - cancel_scheduled
            - past_due
            - billing_restricted
            - canceled
        can_manage_billing:
          type: boolean
    AccountBillingPlans:
      type: object
      additionalProperties: false
      required:
        - free_workspace_available
        - free_workspace_id
        - workspaces
        - truncated
        - next_cursor
      properties:
        free_workspace_available:
          type: boolean
        free_workspace_id:
          oneOf:
            - $ref: "#/components/schemas/Identifier"
            - type: "null"
        workspaces:
          type: array
          maxItems: 100
          items:
            $ref: "#/components/schemas/AccountBillingWorkspace"
        truncated:
          type: boolean
        next_cursor:
          oneOf:
            - $ref: "#/components/schemas/Cursor"
            - type: "null"
    BillingCheckoutRequest:
      type: object
      additionalProperties: false
      required:
        - plan
        - cadence
      properties:
        plan:
          type: string
          enum:
            - personal
            - team
            - growth
        cadence:
          type: string
          enum:
            - monthly
            - annual
    BillingCheckoutSession:
      type: object
      additionalProperties: false
      required:
        - url
        - reference
      properties:
        url:
          type: string
          format: uri
          pattern: ^https://
        reference:
          type: string
          minLength: 1
          maxLength: 255
    BillingPortalSession:
      type: object
      additionalProperties: false
      required:
        - url
      properties:
        url:
          type: string
          format: uri
          pattern: ^https://
    SandboxKnowledgeCreditTopUpRequest:
      type: object
      additionalProperties: false
      required:
        - credits
      properties:
        credits:
          type: integer
          format: int64
          enum:
            - 150
            - 325
            - 875
            - 1800
            - 3700
            - 7600
          description: Purchased AI Credit quantity from the accepted launch catalog.
    SandboxKnowledgeCreditTopUp:
      type: object
      additionalProperties: false
      required:
        - id
        - credits
        - state
        - url
      properties:
        id:
          $ref: "#/components/schemas/Identifier"
        credits:
          type: integer
          format: int64
          enum:
            - 150
            - 325
            - 875
            - 1800
            - 3700
            - 7600
        state:
          type: string
          enum:
            - pending
            - granted
            - refunded
            - failed
        url:
          type: string
          format: uri
    ProfilePictureMutation:
      type: object
      additionalProperties: false
      required:
        - profile_picture_url
        - revision
      properties:
        profile_picture_url:
          oneOf:
            - type: string
              format: uri-reference
            - type: "null"
        revision:
          type: integer
          format: int64
          minimum: 1
    WorkspaceIconMutation:
      type: object
      additionalProperties: false
      required:
        - icon_url
        - revision
      properties:
        icon_url:
          oneOf:
            - type: string
              format: uri-reference
            - type: "null"
        revision:
          type: integer
          format: int64
          minimum: 1
    WorkspaceSummary:
      type: object
      additionalProperties: false
      required:
        - id
        - name
        - slug
        - icon_url
        - role
      properties:
        id:
          $ref: "#/components/schemas/Identifier"
        name:
          type: string
        slug:
          type: string
        icon_url:
          oneOf:
            - type: string
              format: uri
              maxLength: 2048
              pattern: ^https://
            - type: "null"
        role:
          $ref: "#/components/schemas/Role"
    WorkspaceSummaryList:
      type: object
      additionalProperties: false
      required:
        - items
        - has_more
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/WorkspaceSummary"
        next_cursor:
          oneOf:
            - $ref: "#/components/schemas/Cursor"
            - type: "null"
        has_more:
          type: boolean
    Member:
      type: object
      additionalProperties: false
      required:
        - membership_id
        - user_id
        - actor_id
        - display_name
        - username
        - avatar_initials
        - avatar_color
        - role
        - state
        - revision
      properties:
        profile_picture_url:
          oneOf:
            - type: string
              maxLength: 2048
            - type: "null"
          description: Workspace-authorized uploaded image route or external HTTPS Account Profile image; null when unset. Optional for compatibility with older servers.
        membership_id:
          $ref: "#/components/schemas/Identifier"
        user_id:
          $ref: "#/components/schemas/Identifier"
        actor_id:
          $ref: "#/components/schemas/Identifier"
        display_name:
          type: string
        username:
          $ref: "#/components/schemas/PublicUsername"
        avatar_initials:
          oneOf:
            - type: string
              minLength: 1
              maxLength: 2
            - type: "null"
          description: Account-owned avatar letters override; null derives letters from the display name.
        avatar_color:
          oneOf:
            - $ref: "#/components/schemas/PaletteColor"
            - type: "null"
          description: Account-owned avatar background; null predates AVATAR-001 and falls back to a deterministic palette pick.
        role:
          $ref: "#/components/schemas/Role"
        state:
          type: string
          enum:
            - active
        revision:
          type: integer
          minimum: 1
    MemberList:
      type: object
      additionalProperties: false
      required:
        - items
        - has_more
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/Member"
        next_cursor:
          oneOf:
            - $ref: "#/components/schemas/Cursor"
            - type: "null"
        has_more:
          type: boolean
    ProjectKey:
      type: string
      pattern: ^[A-Z][A-Z0-9]{1,7}$
      description: |
        Uppercase-normalized ticket prefix used to form human-readable Task
        references (for example `ASSIGN-42`). Unique per Workspace and
        immutable after creation; not a substitute for the opaque `id`.
      examples:
        - ASSIGN
    Project:
      type: object
      additionalProperties: false
      required:
        - id
        - workspace_id
        - owner_actor_id
        - name
        - key
        - path
        - description
        - url
        - start_on
        - target_on
        - project_state_id
        - visual_identity
        - visibility
        - public_id
        - permissions
        - show_cancelled_column
        - next_task_number
        - revision
        - created_at
        - updated_at
      properties:
        status_lifecycle:
          $ref: "#/components/schemas/ResolvedLifecycle"
        id:
          $ref: "#/components/schemas/Identifier"
        workspace_id:
          $ref: "#/components/schemas/Identifier"
        owner_actor_id:
          $ref: "#/components/schemas/Identifier"
          description: Current Actor that owns the Project and serves as its lead. The owner and Workspace administrators may publish or unpublish it; governed owner transfer is separately revision-checked.
        name:
          type: string
          minLength: 1
          maxLength: 100
        key:
          $ref: "#/components/schemas/ProjectKey"
        path:
          type: string
          pattern: ^[a-z0-9]+(?:[a-z0-9-]*[a-z0-9])?$
          minLength: 1
          maxLength: 63
          description: Editable, Workspace-unique URL segment generated from the Project name at creation.
        description:
          oneOf:
            - type: string
              maxLength: 500
            - type: "null"
          description: Optional plain-text Project summary. It is returned only to authenticated Project readers.
        url:
          oneOf:
            - type: string
              format: uri
              maxLength: 2048
              pattern: ^https?://[^\s]+$
            - type: "null"
          description: Optional absolute HTTP or HTTPS Project link. It is returned only to authenticated Project readers.
        start_on:
          oneOf:
            - type: string
              format: date
            - type: "null"
        target_on:
          oneOf:
            - type: string
              format: date
            - type: "null"
        project_state_id:
          description: Selected Workspace Project-state identifier. It remains present when the optional capability is disabled, but cannot be changed until re-enabled.
          oneOf:
            - $ref: "#/components/schemas/Identifier"
            - type: "null"
        visual_identity:
          description: Optional decorative marker. A null value uses the client fallback without persisting it.
          oneOf:
            - $ref: "#/components/schemas/ProjectVisualIdentity"
            - type: "null"
        visibility:
          $ref: "#/components/schemas/ProjectVisibility"
        public_id:
          description: Opaque unlisted identifier present only while the Project is public.
          oneOf:
            - $ref: "#/components/schemas/Identifier"
            - type: "null"
        permissions:
          $ref: "#/components/schemas/ProjectPermissions"
        show_cancelled_column:
          type: boolean
          description: Project-owned Board preference. False omits cancelled-type columns from the ordinary Board.
        task_count:
          type: integer
          minimum: 0
          readOnly: true
          description: Present on Workspace Project list reads. Exact count of all Tasks excluding archived, trashed and purged Tasks; independent of Task pagination. Includes completed and cancelled Tasks. Mutation and detail responses may omit this aggregate.
        last_task_updated_at:
          allOf:
            - $ref: "#/components/schemas/Timestamp"
          readOnly: true
          description: Latest updated_at among the Tasks included in task_count. Omitted when no active Tasks exist or the aggregate was not read.
        label_ids:
          type: array
          maxItems: 20
          items:
            $ref: "#/components/schemas/Identifier"
        next_task_number:
          type: integer
          minimum: 1
        revision:
          type: integer
          minimum: 1
        created_at:
          $ref: "#/components/schemas/Timestamp"
        updated_at:
          $ref: "#/components/schemas/Timestamp"
        archived_at:
          oneOf:
            - $ref: "#/components/schemas/Timestamp"
            - type: "null"
    ProjectOverview:
      type: object
      additionalProperties: false
      required:
        - project
        - members
        - member_count
        - task_counts
        - milestone_counts
        - status_distribution
        - ongoing_work
        - recent_tasks
        - recent_activity
        - recent_documents
        - recent_files
        - freshness
        - degradation
      properties:
        project:
          $ref: "#/components/schemas/Project"
        members:
          type: array
          maxItems: 3
          items:
            $ref: "#/components/schemas/ProjectOverviewMember"
        member_count:
          type: integer
          minimum: 1
          description: Exact authorized Project-member count.
        task_counts:
          $ref: "#/components/schemas/ProjectOverviewTaskCounts"
        milestone_counts:
          $ref: "#/components/schemas/ProjectOverviewMilestoneCounts"
        status_distribution:
          type: array
          maxItems: 100
          items:
            $ref: "#/components/schemas/ProjectOverviewStatus"
        ongoing_work:
          type: array
          maxItems: 2
          description: Current Tasks grouped only by authoritative workflow category; each group contains at most five Tasks.
          items:
            $ref: "#/components/schemas/ProjectOverviewOngoingGroup"
        recent_tasks:
          type: array
          maxItems: 5
          items:
            $ref: "#/components/schemas/ProjectOverviewTask"
        recent_activity:
          type: array
          maxItems: 5
          items:
            $ref: "#/components/schemas/ActivityItem"
        recent_documents:
          type: array
          maxItems: 5
          items:
            $ref: "#/components/schemas/ProjectOverviewDocument"
        recent_files:
          type: array
          maxItems: 5
          items:
            $ref: "#/components/schemas/ProjectOverviewFile"
        freshness:
          $ref: "#/components/schemas/ProjectOverviewFreshness"
        degradation:
          $ref: "#/components/schemas/ProjectOverviewDegradation"
    ProjectOverviewMember:
      type: object
      additionalProperties: false
      required:
        - actor_id
        - display_name
        - role
        - owner
      properties:
        actor_id:
          $ref: "#/components/schemas/Identifier"
        display_name:
          type: string
          minLength: 1
          maxLength: 100
        role:
          $ref: "#/components/schemas/ProjectMemberRole"
        owner:
          type: boolean
    ProjectOverviewTaskCounts:
      type: object
      additionalProperties: false
      required:
        - total
        - open
        - completed
        - overdue
        - unassigned
        - archived
        - trashed
        - purged
        - completion_percentage
      properties:
        total:
          type: integer
          minimum: 0
          description: Active Tasks only.
        open:
          type: integer
          minimum: 0
          description: Active Tasks outside the done category.
        completed:
          type: integer
          minimum: 0
          description: Active Tasks in the done category.
        overdue:
          type: integer
          minimum: 0
          description: Active non-done Tasks before the database calendar date.
        unassigned:
          type: integer
          minimum: 0
          description: Active Tasks without an assignee.
        archived:
          type: integer
          minimum: 0
        trashed:
          type: integer
          minimum: 0
        purged:
          type: integer
          minimum: 0
        completion_percentage:
          type: number
          format: double
          minimum: 0
          maximum: 100
    ProjectOverviewMilestoneCounts:
      type: object
      additionalProperties: false
      required:
        - total
        - completed
        - task_total
        - task_completed
        - task_completion_percentage
      properties:
        total:
          type: integer
          minimum: 0
          description: Non-archived Milestones.
        completed:
          type: integer
          minimum: 0
        task_total:
          type: integer
          minimum: 0
          description: Active Tasks assigned to non-archived Milestones.
        task_completed:
          type: integer
          minimum: 0
        task_completion_percentage:
          type: number
          format: double
          minimum: 0
          maximum: 100
    ProjectOverviewStatus:
      type: object
      additionalProperties: false
      required:
        - id
        - label
        - category
        - status_type
        - icon
        - color
        - position
        - task_count
      properties:
        id:
          $ref: "#/components/schemas/Identifier"
        label:
          type: string
          minLength: 1
          maxLength: 100
        category:
          $ref: "#/components/schemas/StatusCategory"
        status_type:
          $ref: "#/components/schemas/StatusType"
        icon:
          oneOf:
            - type: string
              pattern: ^[a-z][a-z0-9-]{0,63}$
            - type: "null"
        color:
          oneOf:
            - type: string
              pattern: ^(#[0-9A-Fa-f]{6}|(slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)(-(50|[1-9]00|950))?)$
            - type: "null"
        position:
          type: integer
          minimum: 1
        task_count:
          type: integer
          minimum: 0
          description: Active Tasks only.
    ProjectOverviewTask:
      type: object
      additionalProperties: false
      required:
        - id
        - number
        - code
        - title
        - status_id
        - status_label
        - status_category
        - assignee_actor_id
        - assignee_name
        - due_on
        - summary
        - updated_at
      properties:
        id:
          $ref: "#/components/schemas/Identifier"
        number:
          type: integer
          minimum: 1
        code:
          type: string
          pattern: ^[A-Z][A-Z0-9]{1,7}-[1-9][0-9]*$
        title:
          type: string
          minLength: 1
          maxLength: 500
        status_id:
          $ref: "#/components/schemas/Identifier"
        status_label:
          type: string
          minLength: 1
          maxLength: 100
        status_category:
          $ref: "#/components/schemas/StatusCategory"
        assignee_actor_id:
          oneOf:
            - $ref: "#/components/schemas/Identifier"
            - type: "null"
        assignee_name:
          oneOf:
            - type: string
              minLength: 1
              maxLength: 100
            - type: "null"
        due_on:
          oneOf:
            - type: string
              format: date
            - type: "null"
        summary:
          oneOf:
            - $ref: "#/components/schemas/GeneratedResourceSummary"
            - type: "null"
        updated_at:
          $ref: "#/components/schemas/Timestamp"
    ProjectOverviewOngoingGroup:
      type: object
      additionalProperties: false
      required:
        - category
        - tasks
      properties:
        category:
          type: string
          enum:
            - in_progress
            - in_review
        tasks:
          type: array
          maxItems: 5
          items:
            $ref: "#/components/schemas/ProjectOverviewTask"
    ProjectOverviewDocument:
      type: object
      additionalProperties: false
      required:
        - id
        - title
        - path
        - summary
        - updated_at
      properties:
        id:
          $ref: "#/components/schemas/Identifier"
        title:
          type: string
          minLength: 1
          maxLength: 500
        path:
          type: string
          minLength: 1
          maxLength: 63
        summary:
          oneOf:
            - $ref: "#/components/schemas/GeneratedResourceSummary"
            - type: "null"
        updated_at:
          $ref: "#/components/schemas/Timestamp"
    ProjectOverviewFile:
      type: object
      additionalProperties: false
      required:
        - id
        - filename
        - content_type
        - content_length
        - scan_state
        - created_at
      properties:
        id:
          $ref: "#/components/schemas/Identifier"
        filename:
          $ref: "#/components/schemas/AttachmentFilename"
        content_type:
          type: string
          maxLength: 255
        content_length:
          type: integer
          minimum: 1
        scan_state:
          $ref: "#/components/schemas/AttachmentScanState"
        created_at:
          $ref: "#/components/schemas/Timestamp"
    ProjectOverviewFreshness:
      type: object
      additionalProperties: false
      required:
        - generated_at
        - source_updated_at
      properties:
        generated_at:
          $ref: "#/components/schemas/Timestamp"
        source_updated_at:
          $ref: "#/components/schemas/Timestamp"
    ProjectOverviewDegradation:
      type: object
      additionalProperties: false
      required:
        - degraded
        - unavailable_sections
      properties:
        degraded:
          type: boolean
        unavailable_sections:
          type: array
          maxItems: 9
          uniqueItems: true
          items:
            type: string
            enum:
              - members
              - task_counts
              - milestone_counts
              - status_distribution
              - ongoing_work
              - recent_tasks
              - recent_activity
              - recent_documents
              - recent_files
    ProjectCreateRequest:
      type: object
      additionalProperties: false
      required:
        - name
        - key
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 100
        key:
          $ref: "#/components/schemas/ProjectKey"
        path:
          type: string
          pattern: ^[a-z0-9]+(?:[a-z0-9-]*[a-z0-9])?$
          minLength: 1
          maxLength: 63
          description: Optional Workspace-unique URL segment. When omitted, the server derives it from the Project name.
    ProjectRenameRequest:
      type: object
      additionalProperties: false
      minProperties: 1
      maxProperties: 2
      properties:
        show_cancelled_column:
          type: boolean
          description: Project managers may persist whether cancelled-type Board columns are shown.
        name:
          type: string
          minLength: 1
          maxLength: 100
        path:
          type: string
          pattern: ^[a-z0-9]+(?:[a-z0-9-]*[a-z0-9])?$
          minLength: 1
          maxLength: 63
        description:
          oneOf:
            - type: string
              maxLength: 500
            - type: "null"
          description: Send null or an empty string to clear the Project description.
        url:
          oneOf:
            - type: string
              format: uri
              maxLength: 2048
              pattern: ^https?://[^\s]+$
            - type: "null"
          description: Send null or an empty string to clear the Project URL.
        start_on:
          oneOf:
            - type: string
              format: date
            - type: "null"
          description: Must be sent with `target_on`; the pair is revision-checked atomically.
        target_on:
          oneOf:
            - type: string
              format: date
            - type: "null"
          description: Must be sent with `start_on`; when both values are present, it cannot precede `start_on`.
        visual_identity:
          oneOf:
            - $ref: "#/components/schemas/ProjectVisualIdentity"
            - type: "null"
        visibility:
          $ref: "#/components/schemas/ProjectVisibility"
          description: Publishing is permitted only for the Project owner or a Workspace administrator.
        owner_actor_id:
          $ref: "#/components/schemas/Identifier"
          description: Transfers the owner/lead to an active Workspace member. Only the current owner or a Workspace administrator may use it.
        project_state_id:
          description: Selects one active Workspace Project state, or clears the selection with null. The optional Workspace lifecycle capability must be enabled.
          oneOf:
            - $ref: "#/components/schemas/Identifier"
            - type: "null"
      oneOf:
        - required:
            - name
          maxProperties: 1
        - required:
            - path
          maxProperties: 1
        - required:
            - description
          maxProperties: 1
        - required:
            - url
          maxProperties: 1
        - required:
            - visual_identity
          maxProperties: 1
        - required:
            - visibility
          maxProperties: 1
        - required:
            - owner_actor_id
          maxProperties: 1
        - required:
            - project_state_id
          maxProperties: 1
        - required:
            - start_on
            - target_on
          minProperties: 2
          maxProperties: 2
    ProjectVisibility:
      type: string
      enum:
        - workspace
        - private
        - public
      description: Private Projects require explicit Project membership except for governed Workspace owner/admin access. Public affects only the sanitized anonymous projection; authenticated access remains policy-checked.
    ProjectMemberRole:
      type: string
      enum:
        - manager
        - contributor
        - viewer
    ProjectMember:
      type: object
      additionalProperties: false
      required:
        - actor_id
        - display_name
        - role
        - is_owner
        - revision
        - created_at
        - updated_at
      properties:
        actor_id:
          $ref: "#/components/schemas/Identifier"
        display_name:
          type: string
          minLength: 1
          maxLength: 100
        role:
          $ref: "#/components/schemas/ProjectMemberRole"
        is_owner:
          type: boolean
          description: True only for the Project's current owner/lead.
        revision:
          type: integer
          minimum: 1
        created_at:
          $ref: "#/components/schemas/Timestamp"
        updated_at:
          $ref: "#/components/schemas/Timestamp"
    ProjectMemberList:
      allOf:
        - $ref: "#/components/schemas/Page"
        - type: object
          additionalProperties: false
          required:
            - items
            - total_count
          properties:
            total_count:
              type: integer
              minimum: 1
              description: Exact number of members in the Project.
            items:
              type: array
              maxItems: 100
              items:
                $ref: "#/components/schemas/ProjectMember"
    ProjectMemberCreateRequest:
      type: object
      additionalProperties: false
      required:
        - actor_id
        - role
      properties:
        actor_id:
          $ref: "#/components/schemas/Identifier"
        role:
          $ref: "#/components/schemas/ProjectMemberRole"
    ProjectMemberUpdateRequest:
      type: object
      additionalProperties: false
      required:
        - role
      properties:
        role:
          $ref: "#/components/schemas/ProjectMemberRole"
    ProjectLifecyclePolicy:
      type: object
      additionalProperties: false
      required:
        - workspace_id
        - enabled
        - revision
        - updated_at
      properties:
        workspace_id:
          $ref: "#/components/schemas/Identifier"
        enabled:
          type: boolean
        revision:
          type: integer
          minimum: 1
        updated_at:
          $ref: "#/components/schemas/Timestamp"
    ProjectLifecyclePolicyUpdateRequest:
      type: object
      additionalProperties: false
      required:
        - enabled
      properties:
        enabled:
          type: boolean
    ProjectState:
      type: object
      additionalProperties: false
      required:
        - id
        - workspace_id
        - label
        - position
        - archived_at
        - revision
        - created_at
        - updated_at
      properties:
        id:
          $ref: "#/components/schemas/Identifier"
        workspace_id:
          $ref: "#/components/schemas/Identifier"
        label:
          type: string
          minLength: 1
          maxLength: 80
        position:
          type: integer
          minimum: 0
        archived_at:
          oneOf:
            - $ref: "#/components/schemas/Timestamp"
            - type: "null"
        revision:
          type: integer
          minimum: 1
        created_at:
          $ref: "#/components/schemas/Timestamp"
        updated_at:
          $ref: "#/components/schemas/Timestamp"
    ProjectStateList:
      allOf:
        - $ref: "#/components/schemas/Page"
        - type: object
          additionalProperties: false
          required:
            - items
          properties:
            items:
              type: array
              maxItems: 100
              items:
                $ref: "#/components/schemas/ProjectState"
    ProjectStateCreateRequest:
      type: object
      additionalProperties: false
      required:
        - label
      properties:
        label:
          type: string
          minLength: 1
          maxLength: 80
    ProjectStateUpdateRequest:
      type: object
      additionalProperties: false
      minProperties: 1
      maxProperties: 1
      properties:
        label:
          type: string
          minLength: 1
          maxLength: 80
        archived:
          type: boolean
          description: Only `false` is accepted; the server rejects `true`.
    ProjectStateArchiveAndReplaceRequest:
      type: object
      additionalProperties: false
      required:
        - replacement_state_id
      properties:
        replacement_state_id:
          $ref: "#/components/schemas/Identifier"
    ProjectStateMoveRequest:
      type: object
      additionalProperties: false
      required:
        - expected_revision
      properties:
        after_id:
          oneOf:
            - $ref: "#/components/schemas/Identifier"
            - type: "null"
        before_id:
          oneOf:
            - $ref: "#/components/schemas/Identifier"
            - type: "null"
        expected_revision:
          type: integer
          minimum: 1
    ProjectMoveRequest:
      type: object
      additionalProperties: false
      required:
        - expected_revision
      properties:
        after_id:
          oneOf:
            - $ref: "#/components/schemas/Identifier"
            - type: "null"
          description: The Project that must immediately precede the moved Project.
        before_id:
          oneOf:
            - $ref: "#/components/schemas/Identifier"
            - type: "null"
          description: The Project that must immediately follow the moved Project.
        expected_revision:
          type: integer
          minimum: 1
    ProjectMoveConflict:
      type: object
      additionalProperties: false
      required:
        - error
        - current
      properties:
        error:
          $ref: "#/components/schemas/ErrorResponse/properties/error"
        current:
          $ref: "#/components/schemas/Project"
    ProjectPermissions:
      type: object
      additionalProperties: false
      required:
        - can_update
        - can_publish
        - can_manage_members
      properties:
        can_update:
          type: boolean
        can_publish:
          type: boolean
        can_manage_members:
          type: boolean
    ProjectVisualIdentity:
      description: A single allowlisted Lucide icon name or fully-qualified Unicode Emoji 17.0 sequence.
      oneOf:
        - $ref: "#/components/schemas/ProjectIconVisualIdentity"
        - $ref: "#/components/schemas/ProjectEmojiVisualIdentity"
    ProjectIconVisualIdentity:
      type: object
      additionalProperties: false
      required:
        - kind
        - value
      properties:
        kind:
          const: icon
        value:
          type: string
          enum:
            - accessibility
            - activity
            - ad
            - air-vent
            - alarm-clock
            - alarm-clock-check
            - alarm-clock-minus
            - alarm-clock-plus
            - alarm-smoke
            - album
            - ambulance
            - amphora
            - anchor
            - angle
            - antenna
            - anvil
            - aperture
            - apple
            - archive
            - archive-restore
            - archive-x
            - armchair
            - arrows-up-from-line
            - astroid
            - atom
            - audio-lines-x
            - audio-waveform
            - award
            - axe
            - axis-3d
            - baby
            - backpack
            - badge
            - badge-alert
            - badge-cent
            - badge-check
            - badge-dollar-sign
            - badge-euro
            - badge-indian-rupee
            - badge-info
            - badge-japanese-yen
            - badge-minus
            - badge-percent
            - badge-plus
            - badge-pound-sterling
            - badge-question-mark
            - badge-russian-ruble
            - badge-swiss-franc
            - badge-turkish-lira
            - badge-x
            - baggage-claim
            - balloon
            - banana
            - bandage
            - banknote
            - banknote-arrow-down
            - banknote-arrow-up
            - banknote-check
            - banknote-x
            - barcode
            - barrel
            - bath
            - battery
            - battery-charging
            - battery-full
            - battery-low
            - battery-medium
            - battery-plus
            - battery-warning
            - beaker
            - bean
            - bed
            - bed-double
            - bed-single
            - beef
            - beer
            - bell
            - bell-check
            - bell-dot
            - bell-electric
            - bell-minus
            - bell-plus
            - bell-ring
            - biceps-flexed
            - bike
            - binary
            - binoculars
            - biohazard
            - bird
            - birdhouse
            - bitcoin
            - blender
            - blinds
            - blocks
            - bluetooth
            - bluetooth-connected
            - bluetooth-searching
            - bolt
            - bomb
            - bone
            - bone-fracture
            - book
            - book-a
            - book-alert
            - book-audio
            - book-check
            - book-copy
            - book-dashed
            - book-down
            - book-headphones
            - book-heart
            - book-image
            - book-key
            - book-lock
            - book-marked
            - book-minus
            - book-open
            - book-open-check
            - book-open-text
            - book-plus
            - book-search
            - book-text
            - book-type
            - book-up
            - book-up-2
            - book-user
            - book-x
            - bookmark
            - bookmark-check
            - bookmark-minus
            - bookmark-plus
            - bookmark-x
            - boom-box
            - bot-message-square
            - bottle-wine
            - bow-arrow
            - box
            - boxes
            - braces
            - brackets
            - brain-cog
            - brick-wall
            - brick-wall-fire
            - brick-wall-shield
            - briefcase
            - briefcase-business
            - briefcase-conveyor-belt
            - briefcase-medical
            - broccoli
            - broom
            - broom-sparkles
            - brush
            - brush-cleaning
            - bubbles
            - bug
            - bug-play
            - building-2
            - bus
            - bus-front
            - cable
            - cable-car
            - cake
            - cake-slice
            - calculator
            - calendar
            - calendar-1
            - calendar-arrow-down
            - calendar-arrow-up
            - calendar-check-2
            - calendar-cog
            - calendar-days
            - calendar-fold
            - calendar-heart
            - calendar-minus
            - calendar-minus-2
            - calendar-plus-2
            - calendar-range
            - calendar-search
            - calendar-sync
            - calendar-x
            - calendar-x-2
            - calendars
            - camera
            - candy
            - candy-cane
            - cannabis
            - captions
            - car
            - car-front
            - car-taxi-front
            - caravan
            - card-sim
            - carrot
            - cassette-tape
            - castle
            - cat
            - cctv
            - chart-area
            - chart-bar
            - chart-bar-big
            - chart-bar-decreasing
            - chart-bar-increasing
            - chart-bar-stacked
            - chart-candlestick
            - chart-column
            - chart-column-big
            - chart-column-decreasing
            - chart-column-increasing
            - chart-column-stacked
            - chart-gantt
            - chart-line
            - chart-network
            - chart-no-axes-column
            - chart-no-axes-column-decreasing
            - chart-no-axes-column-increasing
            - chart-no-axes-gantt
            - chart-pie
            - chart-scatter
            - chart-spline
            - check-line
            - chef-hat
            - cherry
            - chess-bishop
            - chess-king
            - chess-knight
            - chess-pawn
            - chess-queen
            - chess-rook
            - chevrons-down
            - chevrons-down-up
            - chevrons-left
            - chevrons-left-right
            - chevrons-left-right-ellipsis
            - chevrons-right
            - chevrons-right-left
            - church
            - cigarette
            - circle-check-big
            - circuit-board
            - citrus
            - clapperboard
            - clock
            - clock-1
            - clock-10
            - clock-11
            - clock-12
            - clock-2
            - clock-4
            - clock-5
            - clock-6
            - clock-7
            - clock-8
            - clock-9
            - clock-alert
            - clock-arrow-down
            - clock-arrow-left
            - clock-arrow-right
            - clock-arrow-up
            - clock-fading
            - clock-plus
            - closed-caption
            - cloud
            - cloud-alert
            - cloud-backup
            - cloud-check
            - cloud-cog
            - cloud-drizzle
            - cloud-fog
            - cloud-hail
            - cloud-lightning
            - cloud-moon
            - cloud-moon-rain
            - cloud-rain
            - cloud-rain-wind
            - cloud-snow
            - cloud-sun
            - cloud-sun-rain
            - cloud-sync
            - cloudy
            - clover
            - club
            - code
            - code-2
            - coffee
            - coins
            - combine
            - compass
            - component
            - computer
            - concierge-bell
            - cone
            - construction
            - contact
            - contact-round
            - container
            - cookie
            - cooking-pot
            - copyleft
            - copyright
            - cpu
            - creative-commons
            - credit-card
            - croissant
            - cross
            - crosshair
            - crown
            - cuboid
            - cup-soda
            - currency
            - cylinder
            - dam
            - database
            - database-arrow-down
            - database-arrow-up
            - database-backup
            - database-check
            - database-minus
            - database-plus
            - database-search
            - database-x
            - database-zap
            - decimals-arrow-left
            - decimals-arrow-right
            - dessert
            - diameter
            - diamond
            - diamond-minus
            - diamond-percent
            - diamond-plus
            - dice-1
            - dice-2
            - dice-3
            - dice-4
            - dice-5
            - dice-6
            - dices
            - disc
            - disc-2
            - disc-3
            - disc-album
            - divide
            - dna
            - dock
            - dog
            - dollar-sign
            - donut
            - door-closed
            - door-closed-locked
            - door-open
            - dot
            - drafting-compass
            - drama
            - drill
            - drone
            - droplet
            - droplets
            - drum
            - drumstick
            - dumbbell
            - ear
            - earth
            - earth-lock
            - eclipse
            - egg
            - egg-fried
            - eject
            - ellipse
            - equal-approximately
            - ethernet-port
            - euro
            - ev-charger
            - eye
            - eye-closed
            - eye-dashed
            - face-angry
            - face-expressionless
            - face-grinning
            - face-neutral
            - face-slightly-frowning
            - face-slightly-smiling
            - factory
            - fan
            - feather
            - fence
            - ferris-wheel
            - file-text
            - files
            - film
            - fingerprint-pattern
            - fire-extinguisher
            - fish
            - fish-symbol
            - fishing-hook
            - fishing-rod
            - flag
            - flag-triangle-left
            - flag-triangle-right
            - flame
            - flame-kindling
            - flashlight
            - flask-conical
            - flask-round
            - flower
            - flower-2
            - folder
            - folder-kanban
            - folders
            - footprints
            - forklift
            - form
            - frame
            - fuel
            - funnel-plus
            - funnel-x
            - gallery-horizontal
            - gallery-horizontal-end
            - gallery-thumbnails
            - gallery-vertical
            - gallery-vertical-end
            - gamepad
            - gamepad-2
            - gamepad-directional
            - gauge
            - gavel
            - gem
            - georgian-lari
            - ghost
            - gift
            - git-branch
            - git-branch-minus
            - git-branch-plus
            - git-commit-vertical
            - git-compare
            - git-compare-arrows
            - git-fork
            - git-graph
            - git-merge-conflict
            - git-pull-request-arrow
            - git-pull-request-closed
            - git-pull-request-create
            - git-pull-request-create-arrow
            - git-pull-request-draft
            - glass-water
            - glasses
            - globe-2
            - globe-check
            - globe-lock
            - globe-x
            - goal
            - gpu
            - graduation-cap
            - grape
            - grid-2x2
            - grid-2x2-check
            - grid-2x2-plus
            - grid-2x2-x
            - grid-3x2
            - grid-3x3
            - group
            - guitar
            - ham
            - hamburger
            - hammer
            - hand
            - hand-coins
            - hand-fist
            - hand-grab
            - hand-heart
            - hand-helping
            - hand-metal
            - hand-platter
            - handbag
            - handshake
            - hard-drive
            - hard-drive-download
            - hard-drive-upload
            - hard-hat
            - hat-glasses
            - haze
            - hd
            - hdmi-port
            - heading
            - heading-1
            - heading-2
            - heading-3
            - heading-4
            - heading-5
            - heading-6
            - headphones
            - headset
            - heart
            - heart-crack
            - heart-handshake
            - heart-minus
            - heart-plus
            - heart-pulse
            - heart-x
            - heater
            - helicopter
            - hexagon
            - home
            - hop
            - hospital
            - hotel
            - hourglass
            - house-heart
            - house-plug
            - house-plus
            - house-wifi
            - ice-cream-bowl
            - ice-cream-cone
            - id-card
            - id-card-lanyard
            - image
            - image-down
            - image-minus
            - image-play
            - image-plus
            - image-up
            - image-upscale
            - images
            - import
            - indian-rupee
            - infinity
            - inspection-panel
            - japanese-yen
            - joystick
            - kanban
            - kayak
            - key
            - key-round
            - keyboard
            - keyboard-music
            - lamp
            - lamp-ceiling
            - lamp-desk
            - lamp-floor
            - lamp-wall-down
            - lamp-wall-up
            - land-plot
            - landmark
            - languages
            - laptop
            - laptop-minimal
            - laptop-minimal-check
            - lasso
            - lasso-select
            - layer-arrow-down
            - layer-arrow-up
            - layers-2
            - layers-3
            - layers-arrow-down
            - layers-arrow-up
            - layers-minus
            - layers-plus
            - leaf
            - leafy-green
            - lectern
            - lens-concave
            - lens-convex
            - library
            - library-big
            - life-buoy
            - lightbulb
            - line-dot-right-horizontal
            - line-squiggle
            - line-style
            - link-2
            - list-todo
            - locate
            - locate-fixed
            - lock
            - lock-keyhole-open
            - lock-open
            - logs
            - lollipop
            - luggage
            - magnet
            - mail
            - mail-badge
            - mail-check
            - mail-minus
            - mail-open
            - mail-question-mark
            - mail-search
            - mail-warning
            - mail-x
            - mailbox
            - mails
            - map
            - map-minus
            - map-pin
            - map-pin-check
            - map-pin-check-inside
            - map-pin-house
            - map-pin-minus
            - map-pin-minus-inside
            - map-pin-pen
            - map-pin-plus
            - map-pin-plus-inside
            - map-pin-search
            - map-pin-x
            - map-pin-x-inside
            - map-pinned
            - map-plus
            - mars
            - mars-stroke
            - martini
            - medal
            - megaphone
            - memory-stick
            - merge
            - message-circle
            - message-circle-check
            - message-circle-code
            - message-circle-dashed
            - message-circle-heart
            - message-circle-more
            - message-circle-plus
            - message-circle-question-mark
            - message-circle-reply
            - message-circle-warning
            - message-circle-x
            - message-square
            - message-square-check
            - message-square-code
            - message-square-dashed
            - message-square-diff
            - message-square-dot
            - message-square-heart
            - message-square-lock
            - message-square-more
            - message-square-plus
            - message-square-quote
            - message-square-reply
            - message-square-share
            - message-square-text
            - message-square-warning
            - message-square-x
            - metronome
            - mic-audio-lines
            - mic-signal
            - microchip
            - microscope
            - microwave
            - milestone
            - milk
            - mirror-rectangular
            - mirror-round
            - monitor
            - monitor-check
            - monitor-cloud
            - monitor-cog
            - monitor-dot
            - monitor-down
            - monitor-pause
            - monitor-play
            - monitor-speaker
            - monitor-stop
            - monitor-up
            - monitor-x
            - moon
            - moon-star
            - mosque
            - motorbike
            - mountain
            - mountain-snow
            - mouse
            - mouse-left
            - mouse-right
            - music
            - music-2
            - music-3
            - music-4
            - network
            - newspaper
            - nfc
            - non-binary
            - notebook
            - notebook-pen
            - notebook-tabs
            - notebook-text
            - notepad-text
            - notepad-text-dashed
            - nut
            - octagon
            - octagon-alert
            - octagon-minus
            - octagon-pause
            - omega
            - orbit
            - origami
            - package
            - package-2
            - package-check
            - package-minus
            - package-open
            - package-plus
            - package-search
            - package-x
            - paint-bucket
            - paint-roller
            - paintbrush
            - paintbrush-vertical
            - palette
            - panda
            - panels-left-bottom
            - panels-right-bottom
            - panels-top-left
            - paper-bag
            - parasol
            - parking-meter
            - party-popper
            - paw-print
            - pc-case
            - pen
            - pen-line
            - pen-tool
            - pencil-line
            - pencil-ruler
            - pencil-sparkles
            - pentagon
            - person-standing
            - phi
            - philippine-peso
            - phone
            - phone-call
            - phone-forwarded
            - phone-incoming
            - phone-missed
            - phone-outgoing
            - pi
            - piano
            - pickaxe
            - picture-in-picture
            - picture-in-picture-2
            - pie-chart
            - piggy-bank
            - pill
            - pill-bottle
            - pin
            - pipette
            - pizza
            - plane
            - plane-landing
            - plane-takeoff
            - plug
            - plug-2
            - plug-zap
            - pocket-knife
            - podium
            - popcorn
            - popsicle
            - pound-sterling
            - presentation
            - printer
            - printer-check
            - printer-x
            - projector
            - puzzle
            - pyramid
            - rabbit
            - radar
            - radiation
            - radical
            - radio
            - radio-receiver
            - radio-tower
            - radius
            - rainbow
            - rat
            - receipt
            - receipt-cent
            - receipt-euro
            - receipt-indian-rupee
            - receipt-japanese-yen
            - receipt-pound-sterling
            - receipt-russian-ruble
            - receipt-swiss-franc
            - receipt-text
            - receipt-turkish-lira
            - rectangle-circle
            - rectangle-ellipsis
            - rectangle-goggles
            - rectangle-horizontal
            - rectangle-vertical
            - recycle
            - refrigerator
            - regex
            - remove-formatting
            - repeat
            - repeat-1
            - repeat-2
            - reply
            - reply-all
            - ribbon
            - road
            - rocket
            - rocking-chair
            - roller-coaster
            - rose
            - route
            - router
            - rss
            - ruler
            - ruler-dimension-line
            - russian-ruble
            - sailboat
            - salad
            - sandwich
            - satellite
            - satellite-dish
            - saudi-riyal
            - save-check
            - save-pen
            - save-plus
            - scale
            - scale-3d
            - school
            - scissors
            - scissors-line-dashed
            - scooter
            - screen-share
            - scroll
            - scroll-text
            - search
            - section
            - send-horizontal
            - server
            - server-cog
            - server-crash
            - server-plus
            - settings
            - shapes
            - share
            - share-2
            - sheet
            - shell
            - shelving-unit
            - shield
            - shield-ban
            - shield-cog
            - shield-cog-corner
            - shield-ellipsis
            - shield-half
            - shield-keyhole
            - shield-lock
            - shield-minus
            - shield-plus
            - shield-question-mark
            - shield-user
            - shield-x
            - ship
            - ship-wheel
            - shirt
            - shopping-bag
            - shopping-basket
            - shopping-cart
            - shovel
            - shower-head
            - shredder
            - shrimp
            - shrink
            - shrub
            - shuffle
            - sigma
            - signal
            - signal-zero
            - signature
            - signpost
            - signpost-big
            - siren
            - skull
            - slice
            - smartphone
            - smartphone-charging
            - smartphone-nfc
            - smile
            - snail
            - snowflake
            - soap-dispenser-droplet
            - sofa
            - solar-panel
            - soup
            - space
            - spade
            - sparkle
            - sparkles
            - speaker
            - speech
            - spline
            - spline-pointer
            - split
            - spool
            - sport-shoe
            - spotlight
            - spray-can
            - sprout
            - squares-exclude
            - squares-intersect
            - squares-subtract
            - squares-unite
            - squircle
            - squircle-dashed
            - squirrel
            - stamp
            - star
            - star-check
            - star-half
            - star-minus
            - star-plus
            - star-x
            - stethoscope
            - sticker
            - sticky-note
            - sticky-note-check
            - sticky-note-minus
            - sticky-note-plus
            - sticky-note-x
            - sticky-notes
            - stone
            - store
            - summary
            - sun
            - sun-dim
            - sun-medium
            - sun-moon
            - sun-snow
            - sunrise
            - sunset
            - swatch-book
            - swiss-franc
            - switch-camera
            - sword
            - swords
            - syringe
            - table
            - table-2
            - table-cells-merge
            - table-cells-split
            - table-columns-split
            - table-of-contents
            - table-properties
            - table-rows-split
            - tablet
            - tablet-smartphone
            - tablets
            - tag-plus
            - tag-x
            - tags
            - tally-1
            - tally-2
            - tally-3
            - tally-4
            - tally-5
            - tangent
            - target
            - telescope
            - tent
            - tent-tree
            - terminal
            - test-tube
            - test-tube-diagonal
            - test-tubes
            - theater
            - thermometer
            - thermometer-snowflake
            - thermometer-sun
            - thumbs-down
            - thumbs-up
            - ticket
            - ticket-check
            - ticket-minus
            - ticket-percent
            - ticket-plus
            - ticket-slash
            - ticket-x
            - tickets
            - tickets-plane
            - timeline
            - timer
            - timer-reset
            - toilet
            - tool-case
            - toolbox
            - tornado
            - torus
            - touchpad
            - towel-rack
            - tower-control
            - toy-brick
            - tractor
            - traffic-cone
            - train-front
            - train-front-tunnel
            - train-track
            - tram-front
            - transgender
            - tree-deciduous
            - tree-palm
            - tree-pine
            - trees
            - trending-down
            - trending-up
            - trending-up-down
            - triangle
            - triangle-dashed
            - triangle-right
            - trophy
            - truck
            - truck-electric
            - turkish-lira
            - turntable
            - turtle
            - tv
            - tv-minimal
            - tv-minimal-play
            - type-outline
            - umbrella
            - ungroup
            - university
            - unlink
            - unlink-2
            - usb
            - user
            - user-check
            - user-cog
            - user-key
            - user-lock
            - user-minus
            - user-pen
            - user-round-arrow-left
            - user-round-check
            - user-round-cog
            - user-round-key
            - user-round-minus
            - user-round-pen
            - user-round-plus
            - user-round-search
            - user-round-x
            - user-search
            - user-shield
            - user-star
            - user-x
            - users
            - users-round
            - utensils
            - utensils-crossed
            - utility-pole
            - van
            - variable
            - vault
            - vector-square
            - vegan
            - venetian-mask
            - venus
            - venus-and-mars
            - vibrate
            - video
            - videotape
            - view
            - voicemail
            - volleyball
            - volume
            - volume-1
            - volume-2
            - volume-x
            - vote
            - wallet
            - wallet-cards
            - wallet-minimal
            - wallpaper
            - wand
            - wand-sparkles
            - warehouse
            - washing-machine
            - watch
            - waves
            - waves-arrow-down
            - waves-arrow-up
            - waves-horizontal
            - waves-ladder
            - waves-vertical
            - waypoints
            - webcam
            - webhook
            - weight
            - weight-tilde
            - wheat
            - whole-word
            - wifi
            - wifi-cog
            - wifi-high
            - wifi-low
            - wifi-pen
            - wifi-sync
            - wifi-zero
            - wind
            - wind-arrow-down
            - wine
            - workflow
            - worm
            - wrench
            - x-line-top
            - zap
            - zodiac-aquarius
            - zodiac-aries
            - zodiac-cancer
            - zodiac-capricorn
            - zodiac-gemini
            - zodiac-leo
            - zodiac-libra
            - zodiac-ophiuchus
            - zodiac-pisces
            - zodiac-sagittarius
            - zodiac-scorpio
            - zodiac-taurus
            - zodiac-virgo
    ProjectEmojiVisualIdentity:
      type: object
      additionalProperties: false
      required:
        - kind
        - value
      properties:
        kind:
          const: emoji
        value:
          type: string
          minLength: 1
          maxLength: 64
          description: Exactly one NFC-normalized, fully-qualified RGI Unicode Emoji 17.0 sequence, excluding standalone components and text presentation.
    ProjectCompletionPolicy:
      type: object
      additionalProperties: false
      required:
        - requires_review
        - default_active_status_id
        - default_done_status_id
      properties:
        requires_review:
          type: boolean
          default: false
        default_active_status_id:
          $ref: "#/components/schemas/Identifier"
        default_review_status_id:
          description: Required when `requires_review` is true; must be a Project-applicable `in_review` Status.
          oneOf:
            - $ref: "#/components/schemas/Identifier"
            - type: "null"
        default_done_status_id:
          $ref: "#/components/schemas/Identifier"
    ProjectList:
      type: object
      additionalProperties: false
      required:
        - items
        - has_more
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/Project"
        next_cursor:
          oneOf:
            - $ref: "#/components/schemas/Cursor"
            - type: "null"
        has_more:
          type: boolean
    PublicProjectStatus:
      type: object
      additionalProperties: false
      required:
        - id
        - label
        - category
        - status_type
        - icon
        - color
        - position
        - task_count
      properties:
        id:
          $ref: "#/components/schemas/Identifier"
        label:
          type: string
          minLength: 1
          maxLength: 80
        category:
          $ref: "#/components/schemas/StatusCategory"
        status_type:
          $ref: "#/components/schemas/StatusType"
        icon:
          oneOf:
            - type: string
              pattern: ^[a-z][a-z0-9-]{0,63}$
            - type: "null"
        color:
          oneOf:
            - type: string
              pattern: ^(#[0-9A-Fa-f]{6}|(slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)(-(50|[1-9]00|950))?)$
            - type: "null"
        position:
          type: integer
        task_count:
          type: integer
          minimum: 0
    PublicProject:
      type: object
      additionalProperties: false
      required:
        - public_id
        - name
        - visual_identity
        - statuses
        - updated_at
      properties:
        task_count:
          type: integer
          minimum: 0
          readOnly: true
          description: Exact count of all non-archived, non-trashed, non-purged Tasks, including completed and cancelled Tasks. Independent of the bounded Status distribution.
        public_id:
          $ref: "#/components/schemas/Identifier"
        name:
          type: string
          minLength: 1
          maxLength: 100
        visual_identity:
          oneOf:
            - $ref: "#/components/schemas/ProjectVisualIdentity"
            - type: "null"
        statuses:
          type: array
          maxItems: 100
          items:
            $ref: "#/components/schemas/PublicProjectStatus"
        updated_at:
          $ref: "#/components/schemas/Timestamp"
    StatusCategory:
      type: string
      enum:
        - backlog
        - todo
        - in_progress
        - in_review
        - done
    StatusType:
      type: string
      description: Canonical lifecycle meaning. Custom labels remain presentation; completed and cancelled are terminal.
      enum:
        - backlog
        - unstarted
        - started
        - completed
        - cancelled
    TaskResolution:
      type: string
      description: Current terminal outcome, independent from the custom Status label.
      enum:
        - completed
        - cancelled
    Status:
      type: object
      additionalProperties: false
      required:
        - id
        - workspace_id
        - project_id
        - label
        - category
        - status_type
        - icon
        - color
        - marks_task_resolved
        - position
        - is_required
        - revision
        - created_at
        - updated_at
        - archived_at
      properties:
        status_lifecycle:
          $ref: "#/components/schemas/ResolvedLifecycle"
        id:
          $ref: "#/components/schemas/Identifier"
        workspace_id:
          $ref: "#/components/schemas/Identifier"
        project_id:
          description: Null for a Workspace-wide Status applicable to every Project.
          oneOf:
            - $ref: "#/components/schemas/Identifier"
            - type: "null"
        label:
          type: string
          minLength: 1
          maxLength: 100
        category:
          $ref: "#/components/schemas/StatusCategory"
        status_type:
          $ref: "#/components/schemas/StatusType"
        icon:
          description: Optional Lucide icon slug. Null inherits the lifecycle default.
          oneOf:
            - type: string
              pattern: ^[a-z][a-z0-9-]{0,63}$
            - type: "null"
        color:
          description: Optional shared Tailwind family or pinned shade, retaining legacy six-digit hex colors. A bare family uses step 500 for Status markers; null inherits the lifecycle default.
          oneOf:
            - type: string
              pattern: ^(#[0-9A-Fa-f]{6}|(slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)(-(50|[1-9]00|950))?)$
            - type: "null"
        marks_task_resolved:
          type: boolean
          readOnly: true
        position:
          type: integer
          minimum: 1
        is_required:
          type: boolean
        revision:
          type: integer
          minimum: 1
        created_at:
          $ref: "#/components/schemas/Timestamp"
        updated_at:
          $ref: "#/components/schemas/Timestamp"
        archived_at:
          oneOf:
            - $ref: "#/components/schemas/Timestamp"
            - type: "null"
    StatusList:
      type: object
      additionalProperties: false
      required:
        - items
        - has_more
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/Status"
        next_cursor:
          oneOf:
            - $ref: "#/components/schemas/Cursor"
            - type: "null"
        has_more:
          type: boolean
    StatusCreateRequest:
      type: object
      additionalProperties: false
      required:
        - label
        - category
      properties:
        label:
          type: string
          minLength: 1
          maxLength: 100
        category:
          $ref: "#/components/schemas/StatusCategory"
        status_type:
          $ref: "#/components/schemas/StatusType"
        icon:
          oneOf:
            - type: string
              pattern: ^[a-z][a-z0-9-]{0,63}$
            - type: "null"
        color:
          oneOf:
            - type: string
              pattern: ^(#[0-9A-Fa-f]{6}|(slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)(-(50|[1-9]00|950))?)$
            - type: "null"
    ProjectStatusCreateRequest:
      type: object
      additionalProperties: false
      required:
        - label
        - category
        - position
      properties:
        label:
          type: string
          minLength: 1
          maxLength: 100
        category:
          $ref: "#/components/schemas/StatusCategory"
        status_type:
          $ref: "#/components/schemas/StatusType"
        icon:
          oneOf:
            - type: string
              pattern: ^[a-z][a-z0-9-]{0,63}$
            - type: "null"
        color:
          oneOf:
            - type: string
              pattern: ^(#[0-9A-Fa-f]{6}|(slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)(-(50|[1-9]00|950))?)$
            - type: "null"
        position:
          type: integer
          minimum: 1
    StatusUpdateRequest:
      type: object
      additionalProperties: false
      minProperties: 1
      properties:
        label:
          type: string
          minLength: 1
          maxLength: 100
        category:
          $ref: "#/components/schemas/StatusCategory"
        status_type:
          $ref: "#/components/schemas/StatusType"
        icon:
          oneOf:
            - type: string
              pattern: ^[a-z][a-z0-9-]{0,63}$
            - type: "null"
        color:
          oneOf:
            - type: string
              pattern: ^(#[0-9A-Fa-f]{6}|(slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)(-(50|[1-9]00|950))?)$
            - type: "null"
        archived:
          type: boolean
          description: Only `false` is accepted to restore an archived Status.
    StatusMoveRequest:
      type: object
      additionalProperties: false
      required:
        - expected_revision
      properties:
        after_id:
          oneOf:
            - $ref: "#/components/schemas/Identifier"
            - type: "null"
        before_id:
          oneOf:
            - $ref: "#/components/schemas/Identifier"
            - type: "null"
        expected_revision:
          type: integer
          minimum: 1
    LifecycleKind:
      type: string
      enum:
        - task
        - milestone
        - project
    ResolvedLifecycle:
      type: object
      additionalProperties: false
      description: |
        Materialized lifecycle policy of the entity's current status. Compare
        `stage_revision` and `definition_revision` for staleness; `behaviors`
        is the complete set for the entity kind, so clients never infer policy
        from names or categories. Absent when the selection has no lifecycle
        status. Resolved for a whole page with one batch lookup. Project list
        rows carry identities and revisions only: `stage_name` and `behaviors`
        are omitted there and returned by Project detail and the lifecycle
        definition list.
      required:
        - stage_id
        - stage_revision
        - definition_id
        - definition_revision
      properties:
        stage_id:
          $ref: "#/components/schemas/Identifier"
        stage_name:
          type: string
        stage_revision:
          type: integer
          minimum: 1
        definition_id:
          $ref: "#/components/schemas/Identifier"
        definition_revision:
          type: integer
          minimum: 1
        behaviors:
          type: object
          additionalProperties:
            type: boolean
    LifecycleCompletionTarget:
      type: object
      additionalProperties: false
      required:
        - status_id
      properties:
        status_id:
          $ref: "#/components/schemas/Identifier"
    LifecycleCompletionChoices:
      type: object
      additionalProperties: false
      required:
        - error
        - choices
      properties:
        error:
          type: object
          required:
            - code
            - message
          properties:
            code:
              type: string
              enum:
                - completion_target_required
            message:
              type: string
            request_id:
              type: string
        choices:
          type: array
          items:
            type: object
            additionalProperties: false
            required:
              - status_id
              - name
              - stage_name
              - definition_id
            properties:
              status_id:
                $ref: "#/components/schemas/Identifier"
              name:
                type: string
              stage_name:
                type: string
              definition_id:
                $ref: "#/components/schemas/Identifier"
    LifecycleBehaviorKey:
      type: string
      enum:
        - notify
        - show_active
        - treat_as_closed
        - counts_as_completed
        - allow_changes
        - track_changes
        - resolves_dependencies
        - treat_as_new
        - include_in_progress
      description: Task-applicable lifecycle behavior keys usable as a Task list filter.
    LifecycleBehaviorList:
      type: object
      additionalProperties: false
      required:
        - items
      properties:
        items:
          type: array
          items:
            type: object
            additionalProperties: false
            required:
              - key
              - kinds
              - consumer
            properties:
              key:
                type: string
              kinds:
                type: array
                items:
                  $ref: "#/components/schemas/LifecycleKind"
              consumer:
                type: string
    LifecycleDefinitionList:
      type: object
      additionalProperties: false
      required:
        - items
      properties:
        items:
          type: array
          items:
            type: object
            additionalProperties: false
            required:
              - id
              - kind
              - name
              - revision
              - default_status_id
              - archived_at
              - stages
            properties:
              id:
                $ref: "#/components/schemas/Identifier"
              kind:
                $ref: "#/components/schemas/LifecycleKind"
              name:
                type: string
              revision:
                type: integer
                minimum: 1
              default_status_id:
                oneOf:
                  - $ref: "#/components/schemas/Identifier"
                  - type: "null"
              archived_at:
                type:
                  - string
                  - "null"
                format: date-time
              stages:
                type: array
                items:
                  type: object
                  additionalProperties: false
                  required:
                    - id
                    - name
                    - position
                    - revision
                    - archived_at
                    - behaviors
                    - statuses
                  properties:
                    id:
                      $ref: "#/components/schemas/Identifier"
                    name:
                      type: string
                    position:
                      type: integer
                      minimum: 1
                    revision:
                      type: integer
                      minimum: 1
                    archived_at:
                      type:
                        - string
                        - "null"
                      format: date-time
                    behaviors:
                      type: object
                      description: Complete boolean policy for the definition's kind, keyed by behavior key.
                      additionalProperties:
                        type: boolean
                    statuses:
                      type: array
                      items:
                        type: object
                        additionalProperties: false
                        required:
                          - id
                          - name
                          - position
                          - archived_at
                        properties:
                          id:
                            $ref: "#/components/schemas/Identifier"
                          name:
                            type: string
                          position:
                            type: integer
                            minimum: 1
                          archived_at:
                            type:
                              - string
                              - "null"
                            format: date-time
    LifecycleMappingRequest:
      type: object
      additionalProperties: false
      required:
        - mappings
      properties:
        mappings:
          type: object
          description: Source status ID to destination status ID. A source mapped to itself keeps its ID and entities. Sources are exactly the keys.
          minProperties: 1
          maxProperties: 500
          additionalProperties:
            $ref: "#/components/schemas/Identifier"
        destination_revisions:
          type: object
          description: Apply only. The `destination_revisions` returned by the preview.
          additionalProperties:
            type: integer
            minimum: 1
        acknowledged:
          type: boolean
          description: Apply only. Required when the preview reports `requires_acknowledgement`.
    LifecycleMappingPreview:
      type: object
      additionalProperties: false
      required:
        - definition_id
        - kind
        - source_revision
        - destination_revisions
        - replaces_default
        - requires_acknowledgement
        - entries
      properties:
        definition_id:
          $ref: "#/components/schemas/Identifier"
        kind:
          $ref: "#/components/schemas/LifecycleKind"
        source_revision:
          type: integer
          minimum: 1
        destination_revisions:
          type: object
          additionalProperties:
            type: integer
            minimum: 1
        replaces_default:
          type: boolean
        requires_acknowledgement:
          type: boolean
        entries:
          type: array
          items:
            type: object
            additionalProperties: false
            required:
              - source_status_id
              - destination_status_id
              - preserved
              - entity_count
              - truncated
              - behavior_changes
            properties:
              source_status_id:
                $ref: "#/components/schemas/Identifier"
              destination_status_id:
                $ref: "#/components/schemas/Identifier"
              preserved:
                type: boolean
              entity_count:
                type: integer
                minimum: 0
                maximum: 10000
              truncated:
                type: boolean
              behavior_changes:
                type: array
                items:
                  type: string
    LifecycleMigrationJob:
      type: object
      additionalProperties: false
      required:
        - id
        - definition_id
        - kind
        - status
        - mapping
        - source_revision
        - replaces_default
        - impact_count
        - processed
        - attempt_count
        - error_code
        - applied_revision
        - applied_at
      properties:
        id:
          $ref: "#/components/schemas/Identifier"
        definition_id:
          $ref: "#/components/schemas/Identifier"
        kind:
          $ref: "#/components/schemas/LifecycleKind"
        status:
          type: string
          enum:
            - pending
            - processing
            - retry
            - completed
            - failed
        mapping:
          type: object
          description: Moved sources only.
          additionalProperties:
            $ref: "#/components/schemas/Identifier"
        source_revision:
          type: integer
          minimum: 1
        replaces_default:
          type: boolean
        impact_count:
          type: integer
          minimum: 0
        processed:
          type: integer
          minimum: 0
        attempt_count:
          type: integer
          minimum: 0
        error_code:
          type:
            - string
            - "null"
        applied_revision:
          type:
            - integer
            - "null"
        applied_at:
          type:
            - string
            - "null"
          format: date-time
    StatusLifecyclePreview:
      type: object
      additionalProperties: false
      required:
        - status_id
        - current_status_type
        - target_status_type
        - affected_task_count
        - synchronous_limit
        - allowed
      properties:
        status_id:
          $ref: "#/components/schemas/Identifier"
        current_status_type:
          $ref: "#/components/schemas/StatusType"
        target_status_type:
          $ref: "#/components/schemas/StatusType"
        affected_task_count:
          type: integer
          minimum: 0
        synchronous_limit:
          type: integer
          minimum: 1
        allowed:
          type: boolean
    StatusArchiveAndReplaceRequest:
      type: object
      additionalProperties: false
      required:
        - replacement_status_id
      properties:
        replacement_status_id:
          $ref: "#/components/schemas/Identifier"
    TaskPriority:
      type: string
      enum:
        - none
        - low
        - medium
        - high
        - urgent
    MilestoneStage:
      type: object
      additionalProperties: false
      required:
        - id
        - label
        - status
        - archived
      properties:
        id:
          type: string
          minLength: 1
          maxLength: 64
          pattern: ^[a-zA-Z0-9_-]{1,64}$
        label:
          type: string
          minLength: 1
          maxLength: 80
        status:
          type: string
          enum:
            - planned
            - active
            - completed
            - cancelled
        archived:
          type: boolean
    MilestoneLifecycle:
      type: object
      additionalProperties: false
      required:
        - workspace_id
        - stages
        - revision
      properties:
        workspace_id:
          $ref: "#/components/schemas/Identifier"
        stages:
          type: array
          minItems: 4
          maxItems: 100
          items:
            $ref: "#/components/schemas/MilestoneStage"
        revision:
          type: integer
          minimum: 1
    MilestoneLifecycleUpdateRequest:
      type: object
      additionalProperties: false
      required:
        - stages
      properties:
        stages:
          type: array
          minItems: 4
          maxItems: 100
          items:
            $ref: "#/components/schemas/MilestoneStage"
        replacements:
          description: Source stage ID to same-category active replacement stage ID for atomic archival migration.
          type: object
          maxProperties: 100
          additionalProperties:
            type: string
            minLength: 1
            maxLength: 64
    Milestone:
      type: object
      additionalProperties: false
      required:
        - id
        - workspace_id
        - project_id
        - name
        - description
        - due_on
        - status
        - revision
        - created_at
        - updated_at
        - archived_at
      properties:
        status_lifecycle:
          $ref: "#/components/schemas/ResolvedLifecycle"
        stage_id:
          description: Optional stable Workspace Milestone stage ID. Must select an active stage whose category agrees with status; null clears the selection. An explicit legacy status update without stage_id clears a custom selection.
          type:
            - string
            - "null"
          minLength: 1
          maxLength: 64
        id:
          $ref: "#/components/schemas/Identifier"
        workspace_id:
          $ref: "#/components/schemas/Identifier"
        project_id:
          $ref: "#/components/schemas/Identifier"
        name:
          type: string
          minLength: 1
          maxLength: 100
        description:
          oneOf:
            - type: string
              maxLength: 2000
            - type: "null"
        due_on:
          oneOf:
            - type: string
              format: date
            - type: "null"
        status:
          type: string
          enum:
            - planned
            - active
            - completed
            - cancelled
        revision:
          type: integer
          minimum: 1
        created_at:
          $ref: "#/components/schemas/Timestamp"
        updated_at:
          $ref: "#/components/schemas/Timestamp"
        archived_at:
          oneOf:
            - $ref: "#/components/schemas/Timestamp"
            - type: "null"
        progress:
          $ref: "#/components/schemas/MilestoneProgress"
        milestone_number:
          description: Stable Project-local positive signed-64-bit number as canonical decimal text (maximum 9223372036854775807), independent of Task numbering and rank. Null or absent while a historical Milestone awaits reference backfill; never derive it client-side.
          type:
            - string
            - "null"
          pattern: ^[1-9][0-9]{0,18}$
          maxLength: 19
          example: "1"
        code:
          description: Canonical uppercase Project-key Milestone reference, for example FRS-M1. Stable across rename, reorder and archive/restore. Null or absent with milestone_number during historical backfill. UUID selectors and foreign keys remain authoritative; knowing a code grants no access.
          type:
            - string
            - "null"
          pattern: ^[A-Z][A-Z0-9]{1,7}-M[1-9][0-9]{0,18}$
          maxLength: 29
          example: FRS-M1
    MilestoneProgress:
      type: object
      additionalProperties: false
      required:
        - total_tasks
        - completed_tasks
        - incomplete_tasks
        - tasks_by_category
      properties:
        total_tasks:
          type: integer
          minimum: 0
        completed_tasks:
          type: integer
          minimum: 0
        incomplete_tasks:
          type: integer
          minimum: 0
        tasks_by_category:
          type: object
          additionalProperties:
            type: integer
            minimum: 0
    MilestoneCreateRequest:
      type: object
      additionalProperties: false
      required:
        - name
      properties:
        stage_id:
          description: Optional stable Workspace Milestone stage ID. Must select an active stage whose category agrees with status; null clears the selection. An explicit legacy status update without stage_id clears a custom selection.
          type:
            - string
            - "null"
          minLength: 1
          maxLength: 64
        name:
          type: string
          minLength: 1
          maxLength: 100
        description:
          oneOf:
            - type: string
              maxLength: 2000
            - type: "null"
        due_on:
          oneOf:
            - type: string
              format: date
            - type: "null"
        status:
          type: string
          enum:
            - planned
            - active
            - completed
            - cancelled
          default: planned
    MilestoneUpdateRequest:
      type: object
      additionalProperties: false
      minProperties: 1
      properties:
        stage_id:
          description: Optional stable Workspace Milestone stage ID. Must select an active stage whose category agrees with status; null clears the selection. An explicit legacy status update without stage_id clears a custom selection.
          type:
            - string
            - "null"
          minLength: 1
          maxLength: 64
        name:
          type: string
          minLength: 1
          maxLength: 100
        description:
          oneOf:
            - type: string
              maxLength: 2000
            - type: "null"
        due_on:
          oneOf:
            - type: string
              format: date
            - type: "null"
        status:
          type: string
          enum:
            - planned
            - active
            - completed
            - cancelled
        archived:
          type: boolean
        after_id:
          oneOf:
            - $ref: "#/components/schemas/Identifier"
            - type: "null"
        before_id:
          oneOf:
            - $ref: "#/components/schemas/Identifier"
            - type: "null"
    MilestoneList:
      type: object
      additionalProperties: false
      required:
        - items
        - has_more
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/Milestone"
        next_cursor:
          oneOf:
            - $ref: "#/components/schemas/Cursor"
            - type: "null"
        has_more:
          type: boolean
    TaskRelation:
      type: object
      additionalProperties: false
      required:
        - id
        - project_id
        - source_task_id
        - target_task_id
        - type
      properties:
        id:
          $ref: "#/components/schemas/Identifier"
        project_id:
          $ref: "#/components/schemas/Identifier"
        source_task_id:
          $ref: "#/components/schemas/Identifier"
        target_task_id:
          $ref: "#/components/schemas/Identifier"
        type:
          type: string
          enum:
            - blocks
            - duplicates
            - relates
            - parent
    TaskRelationCreateRequest:
      type: object
      additionalProperties: false
      required:
        - target_task_id
        - type
      properties:
        target_task_id:
          $ref: "#/components/schemas/Identifier"
        type:
          type: string
          enum:
            - blocks
            - duplicates
            - relates
            - parent
    TaskRelationUpdateRequest:
      type: object
      additionalProperties: false
      required:
        - type
      properties:
        type:
          type: string
          enum:
            - blocks
            - duplicates
            - relates
            - parent
    RelatedTaskSummary:
      type: object
      additionalProperties: false
      required:
        - id
        - code
        - title
        - status_id
      properties:
        id:
          $ref: "#/components/schemas/Identifier"
        code:
          type: string
          description: Project-key-prefixed Task code, e.g. FRS-1.
        title:
          type: string
        status_id:
          $ref: "#/components/schemas/Identifier"
    TaskRelationListItem:
      type: object
      additionalProperties: false
      required:
        - id
        - project_id
        - source_task_id
        - target_task_id
        - type
        - related_task
      properties:
        id:
          $ref: "#/components/schemas/Identifier"
        project_id:
          $ref: "#/components/schemas/Identifier"
        source_task_id:
          $ref: "#/components/schemas/Identifier"
        target_task_id:
          $ref: "#/components/schemas/Identifier"
        type:
          type: string
          enum:
            - blocks
            - duplicates
            - relates
            - parent
        related_task:
          description: Summary of whichever participant is not the Task this relation was listed for.
          $ref: "#/components/schemas/RelatedTaskSummary"
    DocumentWorkLink:
      type: object
      additionalProperties: false
      required:
        - id
        - document_id
        - document_title
        - document_path
        - target_kind
        - target_id
        - target_title
        - target_code
        - project_id
        - project_path
        - can_remove
      properties:
        id:
          $ref: "#/components/schemas/Identifier"
        document_id:
          $ref: "#/components/schemas/Identifier"
        document_title:
          type: string
        document_path:
          type: string
        target_kind:
          type: string
          enum:
            - task
            - milestone
        target_id:
          $ref: "#/components/schemas/Identifier"
        target_title:
          type: string
        target_code:
          type: string
          description: Human-readable Task code; empty for Milestones until their reference projection is available.
        project_id:
          $ref: "#/components/schemas/Identifier"
        project_path:
          type: string
        can_remove:
          type: boolean
          description: Current write authority for both participants; rechecked on mutation.
    DocumentWorkLinkList:
      type: object
      additionalProperties: false
      required:
        - items
        - has_more
      properties:
        items:
          type: array
          maxItems: 100
          items:
            $ref: "#/components/schemas/DocumentWorkLink"
        next_cursor:
          oneOf:
            - $ref: "#/components/schemas/Cursor"
            - type: "null"
        has_more:
          type: boolean
    TaskRelationList:
      type: object
      additionalProperties: false
      required:
        - items
        - has_more
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/TaskRelationListItem"
        next_cursor:
          oneOf:
            - $ref: "#/components/schemas/Cursor"
            - type: "null"
        has_more:
          type: boolean
    IntegrationProvider:
      type: object
      additionalProperties: false
      required:
        - key
        - name
        - description
        - categories
        - capabilities
        - availability
        - official
        - supports_multiple_installations
        - supports_user_connections
      properties:
        key:
          type: string
          pattern: ^[a-z][a-z0-9_]*$
          minLength: 2
          maxLength: 50
        name:
          type: string
          minLength: 1
          maxLength: 100
        description:
          type: string
          minLength: 1
          maxLength: 500
        categories:
          type: array
          minItems: 1
          maxItems: 8
          uniqueItems: true
          items:
            type: string
            enum:
              - source_control
              - communication
              - observability
              - support
              - incident_management
              - automation
        capabilities:
          type: array
          maxItems: 16
          uniqueItems: true
          items:
            type: string
            pattern: ^[a-z][a-z0-9_]*$
            maxLength: 80
        availability:
          type: string
          enum:
            - preview
            - planned
        official:
          type: boolean
        supports_multiple_installations:
          type: boolean
        supports_user_connections:
          type: boolean
    IntegrationProviderList:
      type: object
      additionalProperties: false
      required:
        - items
        - next_cursor
        - has_more
      properties:
        items:
          type: array
          maxItems: 100
          items:
            $ref: "#/components/schemas/IntegrationProvider"
        next_cursor:
          oneOf:
            - $ref: "#/components/schemas/Cursor"
            - type: "null"
        has_more:
          type: boolean
          description: The current catalog is bounded and returns false.
    IntegrationInstallation:
      type: object
      additionalProperties: false
      required:
        - id
        - workspace_id
        - provider_key
        - display_name
        - status
        - health
        - revision
        - created_at
        - updated_at
      properties:
        id:
          $ref: "#/components/schemas/Identifier"
        workspace_id:
          $ref: "#/components/schemas/Identifier"
        provider_key:
          type: string
          pattern: ^[a-z][a-z0-9_]*$
          minLength: 2
          maxLength: 50
        display_name:
          type: string
          minLength: 1
          maxLength: 160
        status:
          type: string
          enum:
            - configuring
            - active
            - degraded
            - paused
            - revoked
        health:
          type: string
          enum:
            - setup_incomplete
            - healthy
            - needs_attention
            - paused
          description: Display-safe server-derived health for the current lifecycle. An active GitLab installation linked to a non-online private connector reports needs_attention from stored connector state.
        revision:
          type: integer
          format: int64
          minimum: 1
        created_at:
          $ref: "#/components/schemas/Timestamp"
        updated_at:
          $ref: "#/components/schemas/Timestamp"
    IntegrationInstallationList:
      type: object
      additionalProperties: false
      required:
        - items
        - next_cursor
        - has_more
      properties:
        items:
          type: array
          maxItems: 100
          items:
            $ref: "#/components/schemas/IntegrationInstallation"
        next_cursor:
          oneOf:
            - $ref: "#/components/schemas/Cursor"
            - type: "null"
        has_more:
          type: boolean
    IntegrationInstallationManagement:
      type: object
      additionalProperties: false
      required:
        - installation_id
        - management_url
        - repository_selection
        - granted_permissions
        - fetched_at
      properties:
        installation_id:
          $ref: "#/components/schemas/Identifier"
        management_url:
          type: string
          format: uri
          pattern: ^https://
          maxLength: 2048
          description: Provider-owned settings URL validated against the configured provider origin.
        repository_selection:
          type: string
          enum:
            - all
            - selected
            - unknown
        granted_permissions:
          type: array
          maxItems: 100
          uniqueItems: true
          items:
            type: string
            pattern: ^[a-z_]+:(read|write|admin)$
            maxLength: 100
        fetched_at:
          $ref: "#/components/schemas/Timestamp"
    IntegrationAuthorizationInput:
      type: object
      additionalProperties: false
      required:
        - provider_key
        - authority_mode
        - return_path
      properties:
        provider_key:
          type: string
          pattern: ^[a-z][a-z0-9_]*$
          minLength: 2
          maxLength: 50
        authority_mode:
          type: string
          enum:
            - installation
            - user
        installation_id:
          oneOf:
            - $ref: "#/components/schemas/Identifier"
            - type: "null"
        return_path:
          type: string
          pattern: ^/(?!/)
          minLength: 1
          maxLength: 1024
        gitlab_self_managed:
          oneOf:
            - $ref: "#/components/schemas/GitLabSelfManagedAuthorizationInput"
            - type: "null"
          description: Required only when creating a new GitLab Self-Managed installation. Reconnects reuse the installation's vault-backed configuration.
    GitLabSelfManagedAuthorizationInput:
      type: object
      additionalProperties: false
      required:
        - instance_url
        - connection_mode
        - oauth_client_id
        - oauth_client_secret
      properties:
        instance_url:
          type: string
          format: uri
          pattern: ^https://
          minLength: 9
          maxLength: 2048
          description: Canonical pathless GitLab Self-Managed origin.
        connection_mode:
          type: string
          enum:
            - public
            - private_connector
        oauth_client_id:
          type: string
          minLength: 1
          maxLength: 512
        oauth_client_secret:
          type: string
          minLength: 1
          maxLength: 4096
          writeOnly: true
        webhook_signing_token:
          type: string
          minLength: 1
          maxLength: 4096
          writeOnly: true
          description: Optional Assign signing token. When omitted, the installation uses reconciliation only.
        connector_id:
          oneOf:
            - $ref: "#/components/schemas/Identifier"
            - type: "null"
          description: Required for private_connector and forbidden for public.
    IntegrationPrivateConnectorEnrollmentInput:
      type: object
      additionalProperties: false
      required:
        - display_name
      properties:
        display_name:
          type: string
          minLength: 1
          maxLength: 160
    IntegrationPrivateConnectorEnrollment:
      type: object
      additionalProperties: false
      required:
        - id
        - workspace_id
        - display_name
        - status
        - token
      properties:
        id:
          $ref: "#/components/schemas/Identifier"
        workspace_id:
          $ref: "#/components/schemas/Identifier"
        display_name:
          type: string
          minLength: 1
          maxLength: 160
        status:
          type: string
          enum:
            - offline
        token:
          type: string
          minLength: 32
          maxLength: 512
          writeOnly: true
          description: Returned exactly once.
    IntegrationAuthorization:
      type: object
      additionalProperties: false
      required:
        - id
        - authorization_url
        - state
        - expires_at
      properties:
        id:
          $ref: "#/components/schemas/Identifier"
        authorization_url:
          type: string
          format: uri
          maxLength: 4096
        state:
          type: string
          minLength: 32
          maxLength: 256
          description: Ten-minute single-use state returned only to the initiating client. It must not be persisted, logged, or placed in a URL by Assign clients.
        expires_at:
          $ref: "#/components/schemas/Timestamp"
    IntegrationCredentialCompletionInput:
      type: object
      additionalProperties: false
      required:
        - state
        - credential
      properties:
        state:
          type: string
          minLength: 32
          maxLength: 256
          writeOnly: true
        credential:
          type: string
          minLength: 16
          maxLength: 256
          writeOnly: true
          description: Provider credential submitted only to the authenticated completion endpoint. Clients must not persist, log, or place this value in a URL.
    IntegrationResource:
      type: object
      additionalProperties: false
      required:
        - id
        - workspace_id
        - installation_id
        - resource_type
        - display_name
        - canonical_url
        - status
        - revision
        - created_at
        - updated_at
      properties:
        id:
          $ref: "#/components/schemas/Identifier"
        workspace_id:
          $ref: "#/components/schemas/Identifier"
        installation_id:
          $ref: "#/components/schemas/Identifier"
        resource_type:
          type: string
          pattern: ^[a-z][a-z0-9_]*(?:\.[a-z][a-z0-9_]*)*$
          minLength: 2
          maxLength: 80
        display_name:
          type: string
          minLength: 1
          maxLength: 240
        canonical_url:
          oneOf:
            - type: string
              format: uri
              pattern: ^https://
              maxLength: 2048
            - type: "null"
        status:
          type: string
          enum:
            - active
            - degraded
            - permission_lost
            - remote_deleted
        revision:
          type: integer
          format: int64
          minimum: 1
        created_at:
          $ref: "#/components/schemas/Timestamp"
        updated_at:
          $ref: "#/components/schemas/Timestamp"
    IntegrationResourceList:
      type: object
      additionalProperties: false
      required:
        - items
        - next_cursor
        - has_more
      properties:
        items:
          type: array
          maxItems: 100
          items:
            $ref: "#/components/schemas/IntegrationResource"
        next_cursor:
          oneOf:
            - type: string
              maxLength: 4096
            - type: "null"
        has_more:
          type: boolean
    IntegrationBindingInput:
      type: object
      additionalProperties: false
      required:
        - resource_id
      properties:
        resource_id:
          $ref: "#/components/schemas/Identifier"
    IntegrationBinding:
      type: object
      additionalProperties: false
      required:
        - id
        - workspace_id
        - installation_id
        - resource_id
        - project_id
        - status
        - revision
        - created_at
        - updated_at
      properties:
        id:
          $ref: "#/components/schemas/Identifier"
        workspace_id:
          $ref: "#/components/schemas/Identifier"
        installation_id:
          $ref: "#/components/schemas/Identifier"
        resource_id:
          $ref: "#/components/schemas/Identifier"
        project_id:
          $ref: "#/components/schemas/Identifier"
        status:
          type: string
          enum:
            - active
            - disabled
            - permission_lost
            - remote_deleted
            - disconnected
        revision:
          type: integer
          format: int64
          minimum: 1
        created_at:
          $ref: "#/components/schemas/Timestamp"
        updated_at:
          $ref: "#/components/schemas/Timestamp"
    IntegrationSubscriptionFilter:
      type: object
      additionalProperties: false
      required:
        - field
        - operator
        - values
      properties:
        field:
          type: string
          pattern: ^[a-z][a-z0-9_]*(?:\.[a-z0-9_]+)*$
          maxLength: 100
        operator:
          type: string
          enum:
            - equals
            - not_equals
            - includes
            - excludes
        values:
          type: array
          minItems: 1
          maxItems: 32
          uniqueItems: true
          items:
            type: string
            minLength: 1
            maxLength: 200
    IntegrationSubscriptionConfiguration:
      type: object
      additionalProperties: false
      required:
        - event_types
      properties:
        event_types:
          type: array
          maxItems: 32
          uniqueItems: true
          description: Empty means all incoming event types. For incoming or bidirectional GitHub development subscriptions (github.development_activity or legacy development.activity on a GitHub binding), create/update adds github.pull.request.review and github.workflow.run to a nonempty explicit list and returns the sorted unique union. The union must fit 32 entries or the request fails with 400 invalid_request; other providers and subscription types are unchanged. Existing explicit lists require a revision-checked update to receive the added event types.
          items:
            type: string
            pattern: ^[a-z][a-z0-9_]*(?:\.[a-z0-9_]+)*$
            maxLength: 100
        filters:
          type: array
          maxItems: 16
          items:
            $ref: "#/components/schemas/IntegrationSubscriptionFilter"
        pull_request_status_mappings:
          type: array
          maxItems: 5
          uniqueItems: true
          description: Optional administrator mappings for GitHub pull requests, GitLab merge requests, and Bitbucket pull requests. An empty or omitted array performs no Task status automation.
          items:
            $ref: "#/components/schemas/GitPullRequestStatusMapping"
        sentry_status_mappings:
          type: array
          maxItems: 4
          uniqueItems: true
          description: Optional per-binding mappings from Sentry issue lifecycle states to active Project-applicable Assign Statuses. An empty or omitted array performs no Task status automation.
          items:
            $ref: "#/components/schemas/SentryStatusMapping"
        thread_sync_mode:
          type: string
          enum:
            - link_only
            - incoming
            - outgoing
            - bidirectional
          description: Explicit Slack conversation synchronization mode. Channel binding alone never enables synchronization.
        task_creation_status_id:
          $ref: "#/components/schemas/Identifier"
          description: Active Project-applicable Status used by an explicitly enabled Slack conversation-to-Task or Sentry alert-to-Task route.
        privacy_mode:
          type: string
          enum:
            - selected_snippet
            - access_safe_notification
            - error_aggregate
          description: Server-enforced bounded content policy for communication, notification, or error-aggregate routes.
    GitPullRequestStatusMapping:
      type: object
      additionalProperties: false
      required:
        - provider_state
        - status_id
      properties:
        provider_state:
          type: string
          enum:
            - open
            - draft
            - merged
            - closed
            - reopened
        status_id:
          $ref: "#/components/schemas/Identifier"
    SentryStatusMapping:
      type: object
      additionalProperties: false
      required:
        - provider_state
        - status_id
      properties:
        provider_state:
          type: string
          enum:
            - unresolved
            - regressed
            - resolved
            - ignored
        status_id:
          $ref: "#/components/schemas/Identifier"
    SentryNativeOption:
      type: object
      additionalProperties: false
      required:
        - label
        - value
      properties:
        label:
          type: string
          minLength: 1
          maxLength: 240
        value:
          type: string
          minLength: 1
          maxLength: 1024
        default:
          type: boolean
    SentryNativeIssueRequest:
      type: object
      additionalProperties: true
      required:
        - fields
        - issueId
        - installationId
        - webUrl
        - project
      properties:
        fields:
          type: object
          maxProperties: 8
          additionalProperties:
            type: string
            maxLength: 1024
        issueId:
          type: string
          minLength: 1
          maxLength: 1024
        installationId:
          type: string
          minLength: 1
          maxLength: 512
        webUrl:
          type: string
          format: uri
          pattern: ^https://
          maxLength: 2048
        project:
          type: object
          additionalProperties: true
          required:
            - slug
            - id
          properties:
            slug:
              type: string
              minLength: 1
              maxLength: 100
            id:
              oneOf:
                - type: string
                  minLength: 1
                  maxLength: 1024
                - type: integer
                  format: int64
                  minimum: 1
    SentryNativeIssueResponse:
      type: object
      additionalProperties: false
      required:
        - webUrl
        - project
        - identifier
      properties:
        webUrl:
          type: string
          format: uri
          pattern: ^https://
          maxLength: 2048
        project:
          type: string
          minLength: 1
          maxLength: 8
        identifier:
          type: string
          minLength: 1
          maxLength: 32
    SentryNativeAlertActionRequest:
      type: object
      additionalProperties: true
      required:
        - installationId
        - fields
      properties:
        installationId:
          type: string
          minLength: 1
          maxLength: 512
        fields:
          type: array
          maxItems: 8
          items:
            type: object
            additionalProperties: true
            required:
              - name
              - value
            properties:
              name:
                type: string
                minLength: 1
                maxLength: 100
              value:
                type: string
                minLength: 1
                maxLength: 1024
    SentryBindingReadiness:
      type: object
      additionalProperties: false
      required:
        - ready
        - safe_state
        - enabled_routes
        - provider_online
      properties:
        ready:
          type: boolean
        safe_state:
          type: string
          enum:
            - ready
            - configuration_inactive
            - behavior_required
            - authorization_unavailable
            - provider_unavailable
        enabled_routes:
          type: array
          maxItems: 3
          uniqueItems: true
          items:
            type: string
            enum:
              - sentry.error_updates
              - sentry.alert_to_task
              - sentry.resolution_sync
        provider_online:
          type: boolean
    IntegrationSubscriptionInput:
      type: object
      additionalProperties: false
      required:
        - type
        - direction
        - status
        - configuration
      properties:
        type:
          type: string
          pattern: ^[a-z][a-z0-9_]*(?:\.[a-z0-9_]+)*$
          minLength: 3
          maxLength: 100
        direction:
          type: string
          enum:
            - none
            - incoming
            - outgoing
            - bidirectional
        status:
          type: string
          enum:
            - enabled
            - paused
            - degraded
            - disabled
        configuration:
          $ref: "#/components/schemas/IntegrationSubscriptionConfiguration"
    IntegrationSubscription:
      allOf:
        - $ref: "#/components/schemas/IntegrationSubscriptionInput"
        - type: object
          additionalProperties: false
          required:
            - id
            - workspace_id
            - installation_id
            - binding_id
            - revision
            - created_at
            - updated_at
          properties:
            id:
              $ref: "#/components/schemas/Identifier"
            workspace_id:
              $ref: "#/components/schemas/Identifier"
            installation_id:
              $ref: "#/components/schemas/Identifier"
            binding_id:
              oneOf:
                - $ref: "#/components/schemas/Identifier"
                - type: "null"
            revision:
              type: integer
              format: int64
              minimum: 1
            created_at:
              $ref: "#/components/schemas/Timestamp"
            updated_at:
              $ref: "#/components/schemas/Timestamp"
    IntegrationSubscriptionList:
      type: object
      additionalProperties: false
      required:
        - items
        - next_cursor
        - has_more
      properties:
        items:
          type: array
          maxItems: 100
          items:
            $ref: "#/components/schemas/IntegrationSubscription"
        next_cursor:
          oneOf:
            - $ref: "#/components/schemas/Cursor"
            - type: "null"
        has_more:
          type: boolean
    IntegrationActionDefinition:
      type: object
      additionalProperties: false
      required:
        - provider_key
        - type
        - display_name
        - description
        - authority_mode
        - confirmation_required
        - requires_binding
        - input_schema
      properties:
        provider_key:
          type: string
          pattern: ^[a-z][a-z0-9_]*$
          minLength: 2
          maxLength: 50
        type:
          type: string
          pattern: ^[a-z][a-z0-9_]*(?:\.[a-z0-9_]+)*$
          minLength: 3
          maxLength: 100
        display_name:
          type: string
          minLength: 1
          maxLength: 160
        description:
          type: string
          minLength: 1
          maxLength: 500
        authority_mode:
          type: string
          enum:
            - installation
            - user
            - either
            - none
        confirmation_required:
          type: boolean
        requires_binding:
          type: boolean
        input_schema:
          type: object
          description: Bounded JSON Schema for the action input object.
          additionalProperties: true
    IntegrationActionDefinitionList:
      type: object
      additionalProperties: false
      required:
        - items
        - next_cursor
        - has_more
      properties:
        items:
          type: array
          maxItems: 100
          items:
            $ref: "#/components/schemas/IntegrationActionDefinition"
        next_cursor:
          oneOf:
            - $ref: "#/components/schemas/Cursor"
            - type: "null"
        has_more:
          type: boolean
    IntegrationActionInput:
      type: object
      additionalProperties: false
      required:
        - installation_id
        - action_type
        - confirmed
        - input
      properties:
        installation_id:
          $ref: "#/components/schemas/Identifier"
        binding_id:
          oneOf:
            - $ref: "#/components/schemas/Identifier"
            - type: "null"
        action_type:
          type: string
          pattern: ^[a-z][a-z0-9_]*(?:\.[a-z0-9_]+)*$
          minLength: 3
          maxLength: 100
        authority_mode:
          type: string
          enum:
            - installation
            - user
        confirmed:
          type: boolean
        input:
          type: object
          maxProperties: 128
          additionalProperties: true
          description: Action-specific bounded input matching the JSON Schema returned by listIntegrationActionDefinitions. GitHub Gist actions accept only an explicit Gist ID and Markdown filename; import may include project_id/title, export requires document_id/filename/visibility and conditionally the exact secret-Gist acknowledgement, and link requires task_id/gist_id/filename.
    IntegrationAction:
      type: object
      additionalProperties: false
      required:
        - id
        - workspace_id
        - installation_id
        - binding_id
        - action_type
        - authority_mode
        - status
        - attempt_count
        - safe_error_code
        - result
        - created_at
        - updated_at
        - completed_at
      properties:
        id:
          $ref: "#/components/schemas/Identifier"
        workspace_id:
          $ref: "#/components/schemas/Identifier"
        installation_id:
          $ref: "#/components/schemas/Identifier"
        binding_id:
          oneOf:
            - $ref: "#/components/schemas/Identifier"
            - type: "null"
        action_type:
          type: string
          maxLength: 100
        authority_mode:
          type: string
          enum:
            - installation
            - user
            - either
            - none
        status:
          type: string
          enum:
            - pending
            - processing
            - retry
            - succeeded
            - failed
            - dead_letter
        attempt_count:
          type: integer
          format: int32
          minimum: 0
        safe_error_code:
          oneOf:
            - type: string
              maxLength: 100
            - type: "null"
        result:
          oneOf:
            - type: object
              additionalProperties: true
            - type: "null"
        created_at:
          $ref: "#/components/schemas/Timestamp"
        updated_at:
          $ref: "#/components/schemas/Timestamp"
        completed_at:
          oneOf:
            - $ref: "#/components/schemas/Timestamp"
            - type: "null"
    IntegrationWebhookReceipt:
      type: object
      additionalProperties: false
      required:
        - id
        - status
        - deduplicated
      properties:
        id:
          $ref: "#/components/schemas/Identifier"
        status:
          type: string
          enum:
            - pending
            - processing
            - retry
            - succeeded
            - dead_letter
            - completed
            - ignored
        deduplicated:
          type: boolean
    IntegrationWebhookSynchronousResponse:
      oneOf:
        - $ref: "#/components/schemas/IntegrationWebhookChallenge"
        - $ref: "#/components/schemas/DiscordInteractionResponse"
    IntegrationWebhookChallenge:
      type: object
      additionalProperties: false
      required:
        - challenge
      properties:
        challenge:
          type: string
          minLength: 1
          maxLength: 1024
    DiscordInteractionResponse:
      type: object
      additionalProperties: false
      required:
        - type
      properties:
        type:
          type: integer
          format: int32
          enum:
            - 1
            - 4
            - 5
        data:
          type: object
          maxProperties: 32
          additionalProperties: true
    PersonalIntegrationIdentity:
      type: object
      additionalProperties: false
      required:
        - id
        - workspace_id
        - installation_id
        - provider_key
        - installation_display_name
        - display_name
        - identity_status
        - authorization_status
        - revision
        - created_at
        - updated_at
      properties:
        id:
          $ref: "#/components/schemas/Identifier"
        workspace_id:
          $ref: "#/components/schemas/Identifier"
        installation_id:
          $ref: "#/components/schemas/Identifier"
        provider_key:
          type: string
          pattern: ^[a-z][a-z0-9_]*$
          minLength: 2
          maxLength: 50
        installation_display_name:
          type: string
          minLength: 1
          maxLength: 160
        display_name:
          type: string
          minLength: 1
          maxLength: 160
        identity_status:
          type: string
          enum:
            - active
            - degraded
            - expired
            - disconnected
          description: Lifecycle of the passive provider identity; collection reads exclude disconnected history while the disconnect response returns its terminal state.
        authorization_status:
          type: string
          enum:
            - not_authorized
            - authorized
            - degraded
            - expired
            - revoked
          description: Derived state of the optional delegated User credential, separate from passive identity state.
        revision:
          type: integer
          format: int64
          minimum: 1
        created_at:
          $ref: "#/components/schemas/Timestamp"
        updated_at:
          $ref: "#/components/schemas/Timestamp"
    PersonalIntegrationIdentityList:
      type: object
      additionalProperties: false
      required:
        - items
        - next_cursor
        - has_more
      properties:
        items:
          type: array
          maxItems: 100
          items:
            $ref: "#/components/schemas/PersonalIntegrationIdentity"
        next_cursor:
          oneOf:
            - $ref: "#/components/schemas/Cursor"
            - type: "null"
        has_more:
          type: boolean
    IntegrationProviderRequestInput:
      type: object
      additionalProperties: false
      required:
        - provider_name
      properties:
        provider_name:
          type: string
          minLength: 2
          maxLength: 80
          description: Provider display name after trimming; control characters are rejected.
        use_case:
          type: string
          maxLength: 500
          description: Optional short explanation after trimming; whitespace runs are normalized and remaining control characters are rejected.
    IntegrationProviderRequest:
      type: object
      additionalProperties: false
      required:
        - id
        - workspace_id
        - provider_name
        - use_case
        - created_at
        - updated_at
      properties:
        id:
          $ref: "#/components/schemas/Identifier"
        workspace_id:
          $ref: "#/components/schemas/Identifier"
        provider_name:
          type: string
          minLength: 2
          maxLength: 80
        use_case:
          oneOf:
            - type: string
              minLength: 1
              maxLength: 500
            - type: "null"
        created_at:
          $ref: "#/components/schemas/Timestamp"
        updated_at:
          $ref: "#/components/schemas/Timestamp"
    ProjectIntegrationBinding:
      type: object
      additionalProperties: false
      required:
        - id
        - project_id
        - installation_id
        - provider_key
        - installation_display_name
        - installation_status
        - resource_id
        - resource_type
        - resource_display_name
        - resource_canonical_url
        - resource_status
        - capability_group
        - binding_status
        - subscription_count
        - enabled_subscription_count
        - attention_subscription_count
        - revision
        - created_at
        - updated_at
      properties:
        id:
          $ref: "#/components/schemas/Identifier"
        project_id:
          $ref: "#/components/schemas/Identifier"
        installation_id:
          $ref: "#/components/schemas/Identifier"
        provider_key:
          type: string
          pattern: ^[a-z][a-z0-9_]*$
          minLength: 2
          maxLength: 50
        installation_display_name:
          type: string
          minLength: 1
          maxLength: 160
        installation_status:
          type: string
          enum:
            - configuring
            - active
            - degraded
            - paused
            - revoked
        resource_id:
          $ref: "#/components/schemas/Identifier"
        resource_type:
          type: string
          pattern: ^[a-z][a-z0-9_]*(?:\.[a-z][a-z0-9_]*)*$
          minLength: 2
          maxLength: 80
        resource_display_name:
          type: string
          minLength: 1
          maxLength: 240
        resource_canonical_url:
          oneOf:
            - type: string
              format: uri
              pattern: ^https://
              maxLength: 2048
            - type: "null"
        resource_status:
          type: string
          enum:
            - active
            - degraded
            - permission_lost
            - remote_deleted
        capability_group:
          type: string
          enum:
            - development
            - communication
            - error_tracking
            - customer_support
            - other
        binding_status:
          type: string
          enum:
            - active
            - disabled
            - permission_lost
            - remote_deleted
        subscription_count:
          type: integer
          format: int32
          minimum: 0
        enabled_subscription_count:
          type: integer
          format: int32
          minimum: 0
        attention_subscription_count:
          type: integer
          format: int32
          minimum: 0
        revision:
          type: integer
          format: int64
          minimum: 1
        created_at:
          $ref: "#/components/schemas/Timestamp"
        updated_at:
          $ref: "#/components/schemas/Timestamp"
    ProjectIntegrationBindingList:
      type: object
      additionalProperties: false
      required:
        - items
        - next_cursor
        - has_more
      properties:
        items:
          type: array
          maxItems: 100
          items:
            $ref: "#/components/schemas/ProjectIntegrationBinding"
        next_cursor:
          oneOf:
            - $ref: "#/components/schemas/Cursor"
            - type: "null"
        has_more:
          type: boolean
    RichEntityStatus:
      type: object
      additionalProperties: false
      required:
        - label
        - tone
      properties:
        label:
          type: string
          minLength: 1
          maxLength: 80
        tone:
          type: string
          enum:
            - neutral
            - info
            - success
            - warning
            - danger
    RichEntityMetric:
      type: object
      additionalProperties: false
      required:
        - id
        - label
        - value
      properties:
        id:
          type: string
          minLength: 1
          maxLength: 64
        label:
          type: string
          minLength: 1
          maxLength: 80
        value:
          type: string
          minLength: 1
          maxLength: 120
        unit:
          type: string
          maxLength: 32
    RichEntityProperty:
      type: object
      additionalProperties: false
      required:
        - id
        - label
        - type
        - value
      properties:
        id:
          type: string
          minLength: 1
          maxLength: 64
        label:
          type: string
          minLength: 1
          maxLength: 80
        type:
          type: string
          enum:
            - text
            - number
            - boolean
            - timestamp
            - link
        value:
          type: string
          minLength: 1
          maxLength: 1000
        url:
          type: string
          format: uri
          pattern: ^https://
          maxLength: 2048
          description: Optional canonical provider URL for the displayed property value.
    RichEntityAction:
      type: object
      additionalProperties: false
      required:
        - id
        - label
        - kind
        - url
      properties:
        id:
          type: string
          minLength: 1
          maxLength: 64
        label:
          type: string
          minLength: 1
          maxLength: 80
        kind:
          type: string
          enum:
            - open_external
        url:
          type: string
          format: uri
          pattern: ^https://
          maxLength: 2048
    RichEntitySnapshot:
      type: object
      additionalProperties: false
      required:
        - title
      properties:
        title:
          type: string
          minLength: 1
          maxLength: 240
        summary:
          type: string
          maxLength: 1000
        status:
          $ref: "#/components/schemas/RichEntityStatus"
        metrics:
          type: array
          maxItems: 8
          items:
            $ref: "#/components/schemas/RichEntityMetric"
        properties:
          type: array
          maxItems: 12
          items:
            $ref: "#/components/schemas/RichEntityProperty"
        actions:
          type: array
          maxItems: 8
          items:
            $ref: "#/components/schemas/RichEntityAction"
    RichEntity:
      type: object
      additionalProperties: false
      required:
        - id
        - installation_id
        - provider_key
        - entity_type
        - external_id
        - human_identifier
        - canonical_url
        - display_name
        - snapshot_schema_version
        - snapshot
        - sync_status
        - sync_message
        - last_synced_at
        - last_changed_at
      properties:
        id:
          $ref: "#/components/schemas/Identifier"
        installation_id:
          $ref: "#/components/schemas/Identifier"
        provider_key:
          type: string
          pattern: ^[a-z][a-z0-9_]*$
          minLength: 2
          maxLength: 50
        entity_type:
          type: string
          pattern: ^[a-z][a-z0-9_]*(?:\.[a-z][a-z0-9_]*)+$
          minLength: 3
          maxLength: 80
        external_id:
          type: string
          minLength: 1
          maxLength: 1024
        human_identifier:
          oneOf:
            - type: string
              minLength: 1
              maxLength: 160
            - type: "null"
        canonical_url:
          type: string
          format: uri
          pattern: ^https://
          maxLength: 2048
        display_name:
          type: string
          minLength: 1
          maxLength: 240
        snapshot_schema_version:
          type: integer
          const: 1
        snapshot:
          $ref: "#/components/schemas/RichEntitySnapshot"
        sync_status:
          type: string
          enum:
            - current
            - stale
            - degraded
            - unavailable
        sync_message:
          oneOf:
            - type: string
              minLength: 1
              maxLength: 500
            - type: "null"
        last_synced_at:
          oneOf:
            - $ref: "#/components/schemas/Timestamp"
            - type: "null"
        last_changed_at:
          oneOf:
            - $ref: "#/components/schemas/Timestamp"
            - type: "null"
    RichEntityList:
      type: object
      additionalProperties: false
      required:
        - items
        - next_cursor
        - has_more
      properties:
        items:
          type: array
          maxItems: 100
          items:
            $ref: "#/components/schemas/RichEntity"
        next_cursor:
          oneOf:
            - $ref: "#/components/schemas/Cursor"
            - type: "null"
        has_more:
          type: boolean
    InboxItem:
      type: object
      additionalProperties: false
      required:
        - id
        - workspace_id
        - category
        - resource_type
        - resource_id
        - summary
        - urgency
        - deep_link
        - aggregation_count
        - last_event_at
        - read_at
        - seen_at
        - archived_at
        - revision
        - created_at
      properties:
        id:
          $ref: "#/components/schemas/Identifier"
        workspace_id:
          $ref: "#/components/schemas/Identifier"
        category:
          $ref: "#/components/schemas/NotificationCategory"
        resource_type:
          type: string
        resource_id:
          $ref: "#/components/schemas/Identifier"
        summary:
          type: object
          additionalProperties: true
        urgency:
          type: string
          enum:
            - low
            - normal
            - high
            - critical
        deep_link:
          type: string
          pattern: ^/(?!/)
          maxLength: 512
          description: Resource-relative route; never an external URL.
        aggregation_count:
          type: integer
          minimum: 1
        last_event_at:
          $ref: "#/components/schemas/Timestamp"
        read_at:
          oneOf:
            - $ref: "#/components/schemas/Timestamp"
            - type: "null"
        seen_at:
          oneOf:
            - $ref: "#/components/schemas/Timestamp"
            - type: "null"
        archived_at:
          oneOf:
            - $ref: "#/components/schemas/Timestamp"
            - type: "null"
        revision:
          type: integer
          minimum: 1
        created_at:
          $ref: "#/components/schemas/Timestamp"
    ActivityActor:
      type: object
      additionalProperties: false
      required:
        - display_name
        - automation
      properties:
        id:
          oneOf:
            - $ref: "#/components/schemas/Identifier"
            - type: "null"
        display_name:
          type: string
          maxLength: 100
        automation:
          type: boolean
          description: True when the actor is an Assign agent or service.
    ActivityProvenanceKind:
      type: string
      enum:
        - delegated_connection
        - mcp
        - automation
        - external_actor
    ActivityProvenance:
      type: object
      additionalProperties: false
      required:
        - kind
        - name
      properties:
        kind:
          $ref: "#/components/schemas/ActivityProvenanceKind"
        name:
          type: string
          minLength: 1
          maxLength: 100
          description: Presentation-safe client, provider, automation, or external-actor name.
    ActivityItem:
      type: object
      additionalProperties: false
      required:
        - id
        - occurred_at
        - actor
        - provenance
        - action
        - resource_type
        - resource_id
        - deep_link
        - summary
      properties:
        id:
          $ref: "#/components/schemas/Identifier"
        occurred_at:
          $ref: "#/components/schemas/Timestamp"
        actor:
          $ref: "#/components/schemas/ActivityActor"
        provenance:
          oneOf:
            - $ref: "#/components/schemas/ActivityProvenance"
            - type: "null"
          description: Null for direct first-party activity; otherwise the authoritative mediated-action classification.
        action:
          type: string
          pattern: ^[a-z][a-z0-9_]*(?:\.[a-z][a-z0-9_]*)+$
        resource_type:
          type: string
          minLength: 1
          maxLength: 50
        resource_id:
          $ref: "#/components/schemas/Identifier"
        deep_link:
          type: string
          description: Canonical resource-relative link when the resource has a browser route; otherwise an empty string.
        summary:
          type: object
          additionalProperties: true
          description: Bounded safe scalar metadata; never rich content. A routine Task editing session can set group_kind=session; priority_changed, due_changed, or milestone_changed; and the corresponding scalar previous/current values. Explicit null means no value; a missing transition key means historical evidence was unavailable. Task history may include boolean description_changed and task.development_updated observations with provider, rich_entity_id, entity_type, human_identifier, title, url and provider_state. An observed provider state does not assert the original action time or a human actor.
        count:
          type: integer
          minimum: 1
          description: Additive; servers that predate grouping omit it, so treat a missing value as 1. Number of admitted events this item represents. Greater than 1 for a bulk operation, an editing session, or a run of comments on one Task. The item's `id` and `occurred_at` are those of its newest event.
        operation_id:
          oneOf:
            - $ref: "#/components/schemas/Identifier"
            - type: "null"
          description: Additive. The shared operation identifier of a grouped bulk item when known; otherwise null or absent.
        affected:
          type: array
          maxItems: 25
          items:
            $ref: "#/components/schemas/ActivityAffectedResource"
          description: Additive; treat a missing value as empty. Up to 25 resources touched by a bulk item spanning more than one resource; empty otherwise. Every entry passed the same current-access filter as the item.
    ActivityAffectedResource:
      type: object
      additionalProperties: false
      required:
        - resource_type
        - resource_id
        - deep_link
      properties:
        resource_type:
          type: string
          minLength: 1
          maxLength: 50
        resource_id:
          $ref: "#/components/schemas/Identifier"
        deep_link:
          type: string
          description: Canonical resource-relative link when the resource has a browser route; otherwise an empty string.
    ActivityList:
      type: object
      additionalProperties: false
      required:
        - items
        - has_more
      properties:
        total_count:
          type: integer
          format: int64
          minimum: 0
          description: Authorized collection size before cursor bounds; refreshed at read time. Activity counts admitted events before display grouping and Comments include retained ordinals.
        items:
          type: array
          items:
            $ref: "#/components/schemas/ActivityItem"
        next_cursor:
          oneOf:
            - $ref: "#/components/schemas/Cursor"
            - type: "null"
        has_more:
          type: boolean
    InboxList:
      type: object
      additionalProperties: false
      required:
        - items
        - has_more
        - unread_count
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/InboxItem"
        next_cursor:
          oneOf:
            - $ref: "#/components/schemas/Cursor"
            - type: "null"
        has_more:
          type: boolean
        unread_count:
          type: integer
          minimum: 0
    InboxReadStateRequest:
      type: object
      additionalProperties: false
      required:
        - read
      properties:
        read:
          type: boolean
    NotificationCategory:
      type: string
      enum:
        - assignment
        - mention
        - comment
        - due
        - dependency
        - agent
        - integration
        - invitation
        - security
        - import_export
        - billing
        - administrative
    ProjectDisplayPreferences:
      type: object
      description: Private Account user preference scoped to one opaque Workspace. Unlisted authorized Projects follow the saved custom sequence in incoming projection order. Home Recent and grid/list layout remain independent.
      properties:
        workspace_id:
          type: string
          format: uuid
        sort:
          type: string
          enum:
            - default
            - recent
            - created
            - custom
        project_ids:
          type: array
          maxItems: 1000
          uniqueItems: true
          items:
            type: string
            format: uuid
        revision:
          type: integer
          format: int64
          minimum: 1
        updated_at:
          type: string
          format: date-time
      required:
        - workspace_id
        - sort
        - project_ids
        - revision
        - updated_at
      additionalProperties: false
    ProjectDisplayPreferencesUpdateRequest:
      type: object
      description: Complete replacement of the caller's private Workspace Project display preference, bounded to 64 KiB. Each Project must be unique, readable, active and in the named Workspace.
      properties:
        sort:
          type: string
          enum:
            - default
            - recent
            - created
            - custom
        project_ids:
          type: array
          maxItems: 1000
          uniqueItems: true
          items:
            type: string
            format: uuid
      required:
        - sort
        - project_ids
      additionalProperties: false
    ProjectShortcutSet:
      type: object
      additionalProperties: false
      required:
        - slots
        - customized
        - revision
        - updated_at
      properties:
        slots:
          type: array
          minItems: 9
          maxItems: 9
          items:
            oneOf:
              - $ref: "#/components/schemas/Identifier"
              - type: "null"
        customized:
          type: boolean
          description: False until the user first replaces the server-created default set; clients may use this once to migrate a legacy device-local preference.
        revision:
          type: integer
          format: int64
          minimum: 1
        updated_at:
          $ref: "#/components/schemas/Timestamp"
    ProjectShortcutUpdateRequest:
      type: object
      additionalProperties: false
      required:
        - slots
      properties:
        slots:
          type: array
          minItems: 9
          maxItems: 9
          items:
            oneOf:
              - $ref: "#/components/schemas/Identifier"
              - type: "null"
    NotificationPreference:
      type: object
      additionalProperties: false
      required:
        - category
        - description
        - mandatory
        - web_enabled
        - email_enabled
        - email_available
        - push_enabled
        - push_available
        - delivery_policy
        - revision
        - updated_at
      properties:
        category:
          $ref: "#/components/schemas/NotificationCategory"
        description:
          type: string
          minLength: 1
          maxLength: 240
        mandatory:
          type: boolean
        web_enabled:
          type: boolean
        email_enabled:
          type: boolean
        email_available:
          type: boolean
          description: True only while the source Workspace has the locally materialized paid email entitlement.
        push_enabled:
          type: boolean
        push_available:
          type: boolean
          description: True while the caller has at least one active, recently confirmed endpoint.
        delivery_policy:
          $ref: "#/components/schemas/NotificationDeliveryPolicy"
        revision:
          type: integer
          minimum: 0
        updated_at:
          $ref: "#/components/schemas/Timestamp"
    NotificationPreferenceList:
      type: object
      additionalProperties: false
      required:
        - items
      properties:
        items:
          type: array
          minItems: 12
          maxItems: 12
          items:
            $ref: "#/components/schemas/NotificationPreference"
    NotificationPreferenceUpdateRequest:
      type: object
      additionalProperties: false
      required:
        - web_enabled
        - email_enabled
        - push_enabled
        - delivery_policy
        - expected_revision
      properties:
        web_enabled:
          type: boolean
        email_enabled:
          type: boolean
        push_enabled:
          type: boolean
        delivery_policy:
          $ref: "#/components/schemas/NotificationDeliveryPolicy"
        expected_revision:
          type: integer
          minimum: 0
    NotificationDeliveryPolicy:
      type: string
      enum:
        - immediate
        - daily_digest
        - weekly_digest
    BrowserPushConfiguration:
      type: object
      additionalProperties: false
      required:
        - vapid_public_key
      properties:
        vapid_public_key:
          type: string
          minLength: 86
          maxLength: 88
          description: Base64url-encoded uncompressed P-256 public key.
    BrowserPushEndpointRequest:
      type: object
      additionalProperties: false
      required:
        - endpoint
        - keys
      properties:
        endpoint:
          type: string
          format: uri
          maxLength: 2048
        keys:
          type: object
          additionalProperties: false
          required:
            - p256dh
            - auth
          properties:
            p256dh:
              type: string
              minLength: 86
              maxLength: 88
            auth:
              type: string
              minLength: 22
              maxLength: 43
    PushEndpoint:
      type: object
      additionalProperties: false
      required:
        - id
        - provider
        - environment
        - state
        - confirmed_until
      properties:
        id:
          $ref: "#/components/schemas/Identifier"
        provider:
          type: string
          enum:
            - webpush
        environment:
          type: string
          minLength: 1
          maxLength: 50
        state:
          type: string
          enum:
            - active
        confirmed_until:
          $ref: "#/components/schemas/Timestamp"
    InboxReadStateConvergenceRequest:
      type: object
      additionalProperties: false
      required:
        - cutoff
        - read
      properties:
        cutoff:
          $ref: "#/components/schemas/Timestamp"
        read:
          type: boolean
          description: True marks visible web deliveries read and seen; false marks them seen only.
    InboxReadStateConvergence:
      type: object
      additionalProperties: false
      required:
        - cutoff
        - changed
        - unread_count
      properties:
        cutoff:
          $ref: "#/components/schemas/Timestamp"
        changed:
          type: integer
          minimum: 0
        unread_count:
          type: integer
          minimum: 0
    MyWorkView:
      type: string
      enum:
        - assigned
        - overdue
        - today
        - upcoming
        - completed
    MyWorkPage:
      type: object
      additionalProperties: false
      required:
        - items
        - has_more
        - as_of_date
        - timezone
      properties:
        total_count:
          type: integer
          format: int64
          minimum: 0
          description: Authorized collection size before cursor bounds; refreshed at read time. Activity counts admitted events before display grouping and Comments include retained ordinals.
        items:
          type: array
          items:
            $ref: "#/components/schemas/Task"
        next_cursor:
          oneOf:
            - $ref: "#/components/schemas/Cursor"
            - type: "null"
        has_more:
          type: boolean
        as_of_date:
          type: string
          format: date
        timezone:
          type: string
          description: Persisted IANA timezone used to classify date-only due dates.
    SubscribedTaskPage:
      type: object
      additionalProperties: false
      required:
        - items
        - has_more
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/Task"
        next_cursor:
          oneOf:
            - $ref: "#/components/schemas/Cursor"
            - type: "null"
        has_more:
          type: boolean
    NativeMobileClientId:
      type: string
      minLength: 1
      maxLength: 100
      pattern: ^[A-Za-z0-9._-]+$
      description: Registered public-client identifier. Native clients never have a client secret.
    DeveloperOAuthClientId:
      type: string
      enum:
        - assign-cli
        - assign-jetbrains
        - assign-vscode
      description: Registered first-party developer-client identifier. Public clients have no client secret and cannot exchange one another's grants.
    DeveloperLoopbackRedirectUri:
      type: string
      format: uri
      pattern: ^http://(127\.0\.0\.1|\[::1\]):[1-9][0-9]{0,4}/callback$
      description: Exact loopback callback bound before browser launch. The server additionally validates the port is at most 65535.
    DeveloperOAuthTokenRequest:
      oneOf:
        - $ref: "#/components/schemas/DeveloperAuthorizationCodeTokenRequest"
        - $ref: "#/components/schemas/DeveloperRefreshTokenRequest"
    DeveloperAuthorizationCodeTokenRequest:
      type: object
      additionalProperties: false
      required:
        - grant_type
        - client_id
        - redirect_uri
        - code
        - code_verifier
      properties:
        grant_type:
          type: string
          const: authorization_code
        client_id:
          $ref: "#/components/schemas/DeveloperOAuthClientId"
        redirect_uri:
          $ref: "#/components/schemas/DeveloperLoopbackRedirectUri"
        code:
          type: string
          minLength: 50
          maxLength: 128
          format: password
        code_verifier:
          $ref: "#/components/schemas/PKCEVerifier"
    DeveloperRefreshTokenRequest:
      type: object
      additionalProperties: false
      required:
        - grant_type
        - client_id
        - refresh_token
      properties:
        grant_type:
          type: string
          const: refresh_token
        client_id:
          $ref: "#/components/schemas/DeveloperOAuthClientId"
        refresh_token:
          type: string
          minLength: 50
          maxLength: 128
          format: password
    DeveloperOAuthTokenSet:
      type: object
      additionalProperties: false
      required:
        - access_token
        - refresh_token
        - token_type
        - expires_in
      properties:
        access_token:
          type: string
          minLength: 50
          maxLength: 128
          format: password
        refresh_token:
          type: string
          minLength: 50
          maxLength: 128
          format: password
        token_type:
          type: string
          const: Bearer
        expires_in:
          type: integer
          const: 900
    CliMcpBridgeTokenRequest:
      type: object
      additionalProperties: false
      properties:
        workspace_id:
          $ref: "#/components/schemas/Identifier"
        scopes:
          type: array
          minItems: 1
          maxItems: 2
          uniqueItems: true
          items:
            $ref: "#/components/schemas/PersonalApiTokenScope"
      description: |
        Optional explicit constraints for the local bridge. Omitting a field
        inherits the authenticated Workspace or parent scopes; neither field
        can expand the parent credential.
    CliMcpBridgeTokenResponse:
      type: object
      additionalProperties: false
      required:
        - access_token
        - token_type
        - scopes
        - expires_in
        - workspace_id
      properties:
        access_token:
          type: string
          minLength: 50
          maxLength: 128
          format: password
        token_type:
          type: string
          const: Bearer
        scopes:
          type: array
          minItems: 1
          maxItems: 2
          uniqueItems: true
          items:
            $ref: "#/components/schemas/PersonalApiTokenScope"
        expires_in:
          type: integer
          minimum: 1
          maximum: 900
        workspace_id:
          $ref: "#/components/schemas/Identifier"
    NativeInstallationId:
      type: string
      minLength: 43
      maxLength: 128
      pattern: ^[A-Za-z0-9_-]+$
      description: High-entropy opaque installation binding generated once and retained only in platform-secure storage. Never place it in logs or analytics.
    NativePlatform:
      type: string
      enum:
        - ios
        - android
    DeviceLabel:
      type: string
      minLength: 1
      maxLength: 64
      description: User-provided display label for account security settings. Control characters are rejected and surrounding whitespace is trimmed.
    OAuthState:
      type: string
      minLength: 16
      maxLength: 2048
      description: Opaque client-generated browser return-state. Assign returns it unchanged only to the exact registered callback.
    PKCEChallenge:
      type: string
      minLength: 43
      maxLength: 128
      pattern: ^[A-Za-z0-9_-]+$
      description: Base64url SHA-256 PKCE challenge. `plain` is not supported.
    PKCEVerifier:
      type: string
      minLength: 43
      maxLength: 128
      pattern: ^[A-Za-z0-9._~-]+$
      format: password
      description: PKCE verifier for one native authorization-code exchange. Never log or persist it after exchange.
    NativeMobileTokenRequest:
      oneOf:
        - $ref: "#/components/schemas/NativeAuthorizationCodeTokenRequest"
        - $ref: "#/components/schemas/NativeRefreshTokenRequest"
    NativeAuthorizationCodeTokenRequest:
      type: object
      additionalProperties: false
      required:
        - grant_type
        - client_id
        - installation_id
        - code
        - code_verifier
      properties:
        grant_type:
          type: string
          const: authorization_code
        client_id:
          $ref: "#/components/schemas/NativeMobileClientId"
        installation_id:
          $ref: "#/components/schemas/NativeInstallationId"
        code:
          type: string
          minLength: 43
          maxLength: 128
          format: password
          description: Single-use five-minute authorization code.
        code_verifier:
          $ref: "#/components/schemas/PKCEVerifier"
    NativeRefreshTokenRequest:
      type: object
      additionalProperties: false
      required:
        - grant_type
        - client_id
        - installation_id
        - refresh_token
      properties:
        grant_type:
          type: string
          const: refresh_token
        client_id:
          $ref: "#/components/schemas/NativeMobileClientId"
        installation_id:
          $ref: "#/components/schemas/NativeInstallationId"
        refresh_token:
          type: string
          minLength: 43
          maxLength: 128
          format: password
          description: Current rotating refresh token. Never log or cache outside platform-secure storage.
    NativePasswordLoginRequest:
      type: object
      additionalProperties: false
      required:
        - client_id
        - platform
        - installation_id
        - device_label
        - email
        - password
      properties:
        client_id:
          type: string
          enum:
            - assign-ios
            - assign-android
        platform:
          type: string
          enum:
            - ios
            - android
        installation_id:
          type: string
          minLength: 43
          maxLength: 128
          pattern: ^[A-Za-z0-9_-]+$
          format: password
        device_label:
          type: string
          minLength: 1
          maxLength: 64
        email:
          $ref: "#/components/schemas/EmailAddress"
        password:
          $ref: "#/components/schemas/Password"
    NativePasswordRegisterRequest:
      type: object
      additionalProperties: false
      required:
        - client_id
        - platform
        - installation_id
        - device_label
        - email
        - password
      properties:
        client_id:
          type: string
          enum:
            - assign-ios
            - assign-android
        platform:
          type: string
          enum:
            - ios
            - android
        installation_id:
          type: string
          minLength: 43
          maxLength: 128
          pattern: ^[A-Za-z0-9_-]+$
          format: password
        device_label:
          type: string
          minLength: 1
          maxLength: 64
        email:
          $ref: "#/components/schemas/EmailAddress"
        password:
          $ref: "#/components/schemas/Password"
        display_name:
          type: string
          minLength: 1
          maxLength: 100
    NativePasswordMFAVerifyRequest:
      type: object
      additionalProperties: false
      required:
        - client_id
        - platform
        - installation_id
        - device_label
        - mfa_token
        - code
      properties:
        client_id:
          type: string
          enum:
            - assign-ios
            - assign-android
        platform:
          type: string
          enum:
            - ios
            - android
        installation_id:
          type: string
          minLength: 43
          maxLength: 128
          pattern: ^[A-Za-z0-9_-]+$
          format: password
        device_label:
          type: string
          minLength: 1
          maxLength: 64
        mfa_token:
          $ref: "#/components/schemas/SingleUseToken"
        code:
          $ref: "#/components/schemas/MFACode"
    NativePasswordLoginResult:
      type: object
      additionalProperties: false
      required:
        - mfa_required
      description: When mfa_required is false tokens is present; when true only mfa_token and expires_at are present. Clients must validate the selected branch before persisting anything.
      properties:
        mfa_required:
          type: boolean
        tokens:
          $ref: "#/components/schemas/NativeMobileTokenSet"
        mfa_token:
          $ref: "#/components/schemas/SingleUseToken"
        expires_at:
          type: string
          format: date-time
    NativeMobileTokenSet:
      type: object
      additionalProperties: false
      required:
        - access_token
        - refresh_token
        - token_type
        - expires_in
      properties:
        access_token:
          type: string
          minLength: 43
          maxLength: 128
          format: password
        refresh_token:
          type: string
          minLength: 43
          maxLength: 128
          format: password
        token_type:
          type: string
          const: Bearer
        expires_in:
          type: integer
          minimum: 0
          maximum: 900
          description: Native access-token lifetime in seconds, capped by the credential family absolute expiry. Zero means reauthentication is required.
    PushDeviceRegistration:
      type: object
      additionalProperties: false
      required:
        - provider
        - environment
        - state
        - confirmed_until
      properties:
        provider:
          type: string
          enum:
            - apns
            - fcm
        environment:
          type: string
          enum:
            - development
            - production
        state:
          type: string
          enum:
            - active
            - suppressed
        confirmed_until:
          $ref: "#/components/schemas/Timestamp"
    PushDeviceUpsertRequest:
      type: object
      additionalProperties: false
      required:
        - provider
        - token
        - environment
      properties:
        provider:
          type: string
          enum:
            - apns
            - fcm
        token:
          type: string
          minLength: 1
          maxLength: 4096
          format: password
          description: Provider-issued delivery token. Accepted only to encrypt and register it; never returned.
        environment:
          type: string
          enum:
            - development
            - production
    MobileCredential:
      type: object
      additionalProperties: false
      required:
        - id
        - client_id
        - platform
        - device_label
        - created_at
        - last_refreshed_at
        - expires_at
        - push_registration
      properties:
        id:
          $ref: "#/components/schemas/Identifier"
        client_id:
          $ref: "#/components/schemas/NativeMobileClientId"
        platform:
          $ref: "#/components/schemas/NativePlatform"
        device_label:
          $ref: "#/components/schemas/DeviceLabel"
        created_at:
          $ref: "#/components/schemas/Timestamp"
        last_refreshed_at:
          $ref: "#/components/schemas/Timestamp"
        expires_at:
          $ref: "#/components/schemas/Timestamp"
        push_registration:
          oneOf:
            - $ref: "#/components/schemas/PushDeviceRegistration"
            - type: "null"
    MobileCredentialList:
      type: object
      additionalProperties: false
      required:
        - items
        - has_more
      properties:
        items:
          type: array
          maxItems: 100
          items:
            $ref: "#/components/schemas/MobileCredential"
        next_cursor:
          oneOf:
            - $ref: "#/components/schemas/Cursor"
            - type: "null"
        has_more:
          type: boolean
    TaskAssignmentActor:
      type: object
      additionalProperties: false
      required:
        - id
        - display_name
      properties:
        id:
          $ref: "#/components/schemas/Identifier"
        display_name:
          type: string
          minLength: 1
          maxLength: 120
    TaskAssignmentSource:
      type: string
      enum:
        - direct
        - imported
    TaskAssignmentPeriod:
      type: object
      additionalProperties: false
      required:
        - id
        - actor
        - assigned_by
        - assigned_at
        - unassigned_at
        - source
        - active
      properties:
        id:
          $ref: "#/components/schemas/Identifier"
        actor:
          $ref: "#/components/schemas/TaskAssignmentActor"
        assigned_by:
          oneOf:
            - $ref: "#/components/schemas/TaskAssignmentActor"
            - type: "null"
        assigned_at:
          description: Null only when an imported current assignment has no trustworthy historical instant.
          oneOf:
            - $ref: "#/components/schemas/Timestamp"
            - type: "null"
        unassigned_at:
          oneOf:
            - $ref: "#/components/schemas/Timestamp"
            - type: "null"
        source:
          $ref: "#/components/schemas/TaskAssignmentSource"
        active:
          type: boolean
    TaskAssignmentList:
      type: object
      additionalProperties: false
      required:
        - items
        - next_cursor
        - has_more
      properties:
        items:
          type: array
          maxItems: 100
          items:
            $ref: "#/components/schemas/TaskAssignmentPeriod"
        next_cursor:
          oneOf:
            - $ref: "#/components/schemas/Cursor"
            - type: "null"
        has_more:
          type: boolean
    TaskParticipant:
      type: object
      additionalProperties: false
      required:
        - actor
        - is_creator
        - is_assigned_now
        - was_previously_assigned
        - last_assigned_at
      properties:
        actor:
          $ref: "#/components/schemas/TaskAssignmentActor"
        is_creator:
          type: boolean
        is_assigned_now:
          type: boolean
        was_previously_assigned:
          type: boolean
        last_assigned_at:
          oneOf:
            - $ref: "#/components/schemas/Timestamp"
            - type: "null"
    TaskParticipantList:
      type: object
      additionalProperties: false
      required:
        - items
        - total_count
        - has_more
      properties:
        items:
          type: array
          maxItems: 100
          items:
            $ref: "#/components/schemas/TaskParticipant"
        total_count:
          type: integer
          minimum: 0
        has_more:
          type: boolean
    TaskSubscriptionState:
      type: string
      enum:
        - following
        - muted
        - unfollowed
    TaskSubscriptionAuthority:
      type: string
      enum:
        - automatic
        - manual
    TaskSubscriptionAutomaticReason:
      type: string
      enum:
        - creator
        - assignee
        - commenter
        - mention
    TaskSubscription:
      type: object
      additionalProperties: false
      required:
        - id
        - task_id
        - actor_id
        - state
        - authority
        - automatic_reason
        - policy_revision
        - revision
        - muted_at
        - unfollowed_at
        - created_at
        - updated_at
      properties:
        id:
          $ref: "#/components/schemas/Identifier"
        task_id:
          $ref: "#/components/schemas/Identifier"
        actor_id:
          $ref: "#/components/schemas/Identifier"
        state:
          $ref: "#/components/schemas/TaskSubscriptionState"
        authority:
          $ref: "#/components/schemas/TaskSubscriptionAuthority"
        automatic_reason:
          oneOf:
            - $ref: "#/components/schemas/TaskSubscriptionAutomaticReason"
            - type: "null"
        policy_revision:
          type: integer
          minimum: 1
        revision:
          type: integer
          format: int64
          minimum: 1
        muted_at:
          oneOf:
            - $ref: "#/components/schemas/Timestamp"
            - type: "null"
        unfollowed_at:
          oneOf:
            - $ref: "#/components/schemas/Timestamp"
            - type: "null"
        created_at:
          $ref: "#/components/schemas/Timestamp"
        updated_at:
          $ref: "#/components/schemas/Timestamp"
    TaskSubscriptionEnvelope:
      type: object
      additionalProperties: false
      required:
        - subscription
      properties:
        subscription:
          oneOf:
            - $ref: "#/components/schemas/TaskSubscription"
            - type: "null"
    TaskSubscriber:
      type: object
      additionalProperties: false
      required:
        - subscription_id
        - actor_id
        - display_name
        - following_since
      properties:
        subscription_id:
          $ref: "#/components/schemas/Identifier"
        actor_id:
          $ref: "#/components/schemas/Identifier"
        display_name:
          type: string
          minLength: 1
          maxLength: 120
        following_since:
          $ref: "#/components/schemas/Timestamp"
    TaskSubscriberList:
      type: object
      additionalProperties: false
      required:
        - items
        - next_cursor
        - has_more
      properties:
        items:
          type: array
          maxItems: 100
          items:
            $ref: "#/components/schemas/TaskSubscriber"
        next_cursor:
          oneOf:
            - $ref: "#/components/schemas/Cursor"
            - type: "null"
        has_more:
          type: boolean
    ReversibleCommand:
      type: object
      additionalProperties: false
      required:
        - id
        - type
        - entity_type
        - entity_id
        - description
        - undo_description
        - state
        - undoable_until
        - created_at
      properties:
        id:
          $ref: "#/components/schemas/Identifier"
        type:
          type: string
          pattern: ^[a-z][a-z0-9_]*(?:\.[a-z][a-z0-9_]*)+$
        entity_type:
          type: string
          enum:
            - task
        entity_id:
          $ref: "#/components/schemas/Identifier"
        description:
          type: string
          minLength: 1
          maxLength: 500
        undo_description:
          type: string
          minLength: 1
          maxLength: 500
        state:
          type: string
          enum:
            - done
            - undone
        undoable_until:
          $ref: "#/components/schemas/Timestamp"
        created_at:
          $ref: "#/components/schemas/Timestamp"
    ReversibleCommandExecution:
      type: object
      additionalProperties: false
      required:
        - command
        - task
      properties:
        command:
          $ref: "#/components/schemas/ReversibleCommand"
        task:
          $ref: "#/components/schemas/Task"
    Task:
      type: object
      additionalProperties: false
      required:
        - id
        - workspace_id
        - project_id
        - task_number
        - code
        - status_id
        - rank
        - title
        - priority
        - revision
        - created_at
        - updated_at
        - archived_at
        - trashed_at
        - purge_after
        - purged_at
        - lifecycle
        - resolved_at
        - resolved_by
        - resolution
        - summary
      properties:
        status_lifecycle:
          $ref: "#/components/schemas/ResolvedLifecycle"
        id:
          $ref: "#/components/schemas/Identifier"
        workspace_id:
          $ref: "#/components/schemas/Identifier"
        project_id:
          $ref: "#/components/schemas/Identifier"
        task_number:
          type: integer
          minimum: 1
        code:
          $ref: "#/components/schemas/TaskCode"
        status_id:
          $ref: "#/components/schemas/Identifier"
        rank:
          type: string
          minLength: 1
          description: Server-owned sortable order key within the Task's Project and Status. Clients render it but submit neighbour anchors when moving a Task.
        assignee_actor_id:
          oneOf:
            - $ref: "#/components/schemas/Identifier"
            - type: "null"
        title:
          type: string
          minLength: 1
          maxLength: 500
        priority:
          $ref: "#/components/schemas/TaskPriority"
        description:
          oneOf:
            - $ref: "#/components/schemas/RichTextDocument"
            - type: "null"
        summary:
          description: Revision-bound derived convenience text. It is null when the body is shorter than 300 normalized characters, generation is pending, or the stored projection is stale. It is never evidence or a replacement for description content.
          oneOf:
            - $ref: "#/components/schemas/GeneratedResourceSummary"
            - type: "null"
        due_on:
          oneOf:
            - type: string
              format: date
            - type: "null"
        milestone_id:
          oneOf:
            - $ref: "#/components/schemas/Identifier"
            - type: "null"
        completed_at:
          description: Server-written instant when the Task last entered a done-category Status; null while open or unavailable for a pre-migration record.
          oneOf:
            - $ref: "#/components/schemas/Timestamp"
            - type: "null"
        resolved_at:
          description: Instant the current terminal episode began; cleared when the Task becomes active again.
          oneOf:
            - $ref: "#/components/schemas/Timestamp"
            - type: "null"
        resolved_by:
          description: Actor that started the current terminal episode; null only for migrated or trusted automated provenance gaps.
          oneOf:
            - $ref: "#/components/schemas/Identifier"
            - type: "null"
        resolution:
          description: Terminal outcome for the current resolved episode; null for active Tasks.
          oneOf:
            - $ref: "#/components/schemas/TaskResolution"
            - type: "null"
        label_ids:
          description: Task collections include the complete assignment identifier set; an empty array means no labels. Non-collection responses may omit this field; use the target-label read when absent.
          type: array
          maxItems: 20
          items:
            $ref: "#/components/schemas/Identifier"
        reversible_command:
          description: Present only on a mutation response when the server created an undoable command. Read responses omit it.
          $ref: "#/components/schemas/ReversibleCommand"
        revision:
          type: integer
          minimum: 1
        created_at:
          $ref: "#/components/schemas/Timestamp"
        updated_at:
          $ref: "#/components/schemas/Timestamp"
        archived_at:
          oneOf:
            - $ref: "#/components/schemas/Timestamp"
            - type: "null"
        trashed_at:
          oneOf:
            - $ref: "#/components/schemas/Timestamp"
            - type: "null"
        purge_after:
          description: Fixed retention deadline after which the asynchronous purge sweeper may redact Task content; null outside trash and after purge.
          oneOf:
            - $ref: "#/components/schemas/Timestamp"
            - type: "null"
        purged_at:
          description: Tombstone creation instant. A purged Task retains only stable reference metadata and cannot be restored.
          oneOf:
            - $ref: "#/components/schemas/Timestamp"
            - type: "null"
        lifecycle:
          type: string
          enum:
            - active
            - archived
            - trashed
            - purged
    TaskStartResult:
      type: object
      additionalProperties: false
      required:
        - task
        - reassigned
      properties:
        task:
          $ref: "#/components/schemas/Task"
        reassigned:
          type: boolean
          description: True when Start Work closed another Actor's active assignment period before assigning the caller.
    TaskDuplicateRequest:
      type: object
      additionalProperties: false
      required:
        - copy_assignee
        - copy_dates
        - copy_milestone
        - copy_labels
        - copy_subtasks
        - copy_attachments
      properties:
        target_project_id:
          description: Destination Project. Omit or use null to duplicate inside the source Project.
          oneOf:
            - $ref: "#/components/schemas/Identifier"
            - type: "null"
        target_status_id:
          description: Explicit destination Status for the new root. Omit or use null to preserve the Status in the same Project or map its category across Projects.
          oneOf:
            - $ref: "#/components/schemas/Identifier"
            - type: "null"
        copy_assignee:
          type: boolean
        copy_dates:
          type: boolean
          description: Copy the Task due date.
        copy_milestone:
          type: boolean
        copy_labels:
          type: boolean
        copy_subtasks:
          type: boolean
          description: Copy at most 25 active descendants while preserving the parent hierarchy.
        copy_attachments:
          type: boolean
          description: Link each copied Task to its existing clean attachment objects; stored bytes are not duplicated.
    TaskDuplicateIssue:
      type: object
      additionalProperties: false
      required:
        - code
      properties:
        code:
          type: string
          enum:
            - assignee_unavailable
            - milestone_incompatible
            - label_unavailable
            - label_unmapped
            - subtask_parent_skipped
            - subtask_unavailable
            - subtask_status_incompatible
        source_task_id:
          oneOf:
            - $ref: "#/components/schemas/Identifier"
            - type: "null"
        source_resource_id:
          oneOf:
            - $ref: "#/components/schemas/Identifier"
            - type: "null"
    TaskDuplicateResult:
      type: object
      additionalProperties: false
      required:
        - task
        - copied_task_count
        - copied_subtask_count
        - copied_label_count
        - copied_attachment_count
        - issues
      properties:
        task:
          $ref: "#/components/schemas/Task"
        copied_task_count:
          type: integer
          minimum: 1
          maximum: 26
        copied_subtask_count:
          type: integer
          minimum: 0
          maximum: 25
        copied_label_count:
          type: integer
          minimum: 0
          maximum: 520
        copied_attachment_count:
          type: integer
          minimum: 0
          maximum: 2600
        issues:
          type: array
          maxItems: 650
          items:
            $ref: "#/components/schemas/TaskDuplicateIssue"
    TaskCreateRequest:
      type: object
      additionalProperties: false
      required:
        - title
      properties:
        status_id:
          $ref: "#/components/schemas/Identifier"
          description: Omit to use the Workspace's default Task workflow category.
        assignee_actor_id:
          $ref: "#/components/schemas/Identifier"
          description: Omit to use the Workspace's default Task assignee policy.
        title:
          type: string
          minLength: 1
          maxLength: 500
        priority:
          $ref: "#/components/schemas/TaskPriority"
          description: Omit to use the Workspace's default Task priority.
        description:
          $ref: "#/components/schemas/RichTextDocument"
        due_on:
          type: string
          format: date
        milestone_id:
          $ref: "#/components/schemas/Identifier"
    TaskUpdateRequest:
      type: object
      additionalProperties: false
      description: Every field is optional; an omitted field keeps its current value. Moving to another Project requires a Status that applies to that Project and clears the Project-scoped milestone.
      properties:
        project_id:
          $ref: "#/components/schemas/Identifier"
        status_id:
          $ref: "#/components/schemas/Identifier"
        assignee_actor_id:
          description: An explicit null clears the assignee.
          oneOf:
            - $ref: "#/components/schemas/Identifier"
            - type: "null"
        title:
          type: string
          minLength: 1
          maxLength: 500
        priority:
          $ref: "#/components/schemas/TaskPriority"
        description:
          oneOf:
            - $ref: "#/components/schemas/RichTextDocument"
            - type: "null"
        due_on:
          oneOf:
            - type: string
              format: date
            - type: "null"
        milestone_id:
          oneOf:
            - $ref: "#/components/schemas/Identifier"
            - type: "null"
    BulkTaskUpdateItem:
      type: object
      additionalProperties: false
      required:
        - task_id
        - expected_revision
      properties:
        task_id:
          $ref: "#/components/schemas/Identifier"
        expected_revision:
          type: integer
          minimum: 1
    BulkTaskUpdateChanges:
      type: object
      additionalProperties: false
      minProperties: 1
      properties:
        status_id:
          $ref: "#/components/schemas/Identifier"
        assignee_actor_id:
          oneOf:
            - $ref: "#/components/schemas/Identifier"
            - type: "null"
        priority:
          $ref: "#/components/schemas/TaskPriority"
        due_on:
          oneOf:
            - type: string
              format: date
            - type: "null"
        milestone_id:
          oneOf:
            - $ref: "#/components/schemas/Identifier"
            - type: "null"
    BulkTaskUpdateRequest:
      type: object
      additionalProperties: false
      required:
        - items
        - changes
      properties:
        items:
          type: array
          minItems: 1
          maxItems: 100
          items:
            $ref: "#/components/schemas/BulkTaskUpdateItem"
        changes:
          $ref: "#/components/schemas/BulkTaskUpdateChanges"
    BulkTaskUpdateOutcome:
      type: object
      additionalProperties: false
      required:
        - task_id
        - outcome
      properties:
        task_id:
          $ref: "#/components/schemas/Identifier"
        outcome:
          type: string
          enum:
            - updated
            - not_found
            - conflict
            - validation_failed
            - failed
        task:
          oneOf:
            - $ref: "#/components/schemas/Task"
            - type: "null"
    BulkTaskUpdateResult:
      type: object
      additionalProperties: false
      required:
        - items
      properties:
        items:
          type: array
          maxItems: 100
          items:
            $ref: "#/components/schemas/BulkTaskUpdateOutcome"
    TaskMoveRequest:
      type: object
      additionalProperties: false
      required:
        - status_id
        - expected_revision
      properties:
        status_id:
          $ref: "#/components/schemas/Identifier"
        after_task_id:
          description: The active Task that must immediately precede the moved Task in the destination column.
          oneOf:
            - $ref: "#/components/schemas/Identifier"
            - type: "null"
        before_task_id:
          description: The active Task that must immediately follow the moved Task in the destination column.
          oneOf:
            - $ref: "#/components/schemas/Identifier"
            - type: "null"
        expected_revision:
          type: integer
          minimum: 1
    TaskDescriptionRevision:
      type: object
      additionalProperties: false
      required:
        - task_id
        - revision
        - actor_id
        - change_kind
        - description
        - description_text
        - created_at
      properties:
        task_id:
          $ref: "#/components/schemas/Identifier"
        revision:
          type: integer
          minimum: 1
        actor_id:
          oneOf:
            - $ref: "#/components/schemas/Identifier"
            - type: "null"
        change_kind:
          type: string
          enum:
            - baseline
            - created
            - updated
        description:
          oneOf:
            - $ref: "#/components/schemas/RichTextDocument"
            - type: "null"
        description_text:
          type: string
        created_at:
          $ref: "#/components/schemas/Timestamp"
    TaskDescriptionRevisionList:
      type: object
      additionalProperties: false
      required:
        - items
        - has_more
      properties:
        items:
          type: array
          maxItems: 100
          items:
            $ref: "#/components/schemas/TaskDescriptionRevision"
        next_cursor:
          oneOf:
            - $ref: "#/components/schemas/Cursor"
            - type: "null"
        has_more:
          type: boolean
    TaskMoveConflict:
      type: object
      additionalProperties: false
      required:
        - error
        - current
      properties:
        error:
          $ref: "#/components/schemas/ErrorResponse/properties/error"
        current:
          $ref: "#/components/schemas/Task"
    TaskList:
      type: object
      additionalProperties: false
      required:
        - items
        - has_more
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/Task"
        next_cursor:
          oneOf:
            - $ref: "#/components/schemas/Cursor"
            - type: "null"
        has_more:
          type: boolean
        query_digest:
          type: string
          description: Digest of the normalized authorized query; present with checked consistency.
        collection_version:
          type: string
          description: Opaque Core collection watermark, never a client-provided timestamp.
        visibility_revision:
          type: string
          description: Opaque caller-bound authorization visibility watermark.
        observed_at:
          $ref: "#/components/schemas/Timestamp"
        returned_count:
          type: integer
          minimum: 0
          maximum: 1000
          description: Cumulative rows returned along this checked cursor lineage.
        coverage:
          type: string
          enum:
            - complete
            - partial
            - stale
          description: Complete proves this filtered authorized scan at its observation boundary; partial and stale cannot prove absence.
        limit_reason:
          type: string
          enum:
            - more_pages
            - scan_limit
            - collection_or_visibility_changed
    ProjectTaskList:
      type: object
      additionalProperties: false
      required:
        - items
        - has_more
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/Task"
        next_cursor:
          oneOf:
            - $ref: "#/components/schemas/Cursor"
            - type: "null"
        has_more:
          type: boolean
        counts:
          description: Present for number-ordered history pages.
          $ref: "#/components/schemas/ProjectTaskCounts"
        column:
          $ref: "#/components/schemas/ProjectTaskColumn"
    ProjectTaskColumn:
      type: object
      additionalProperties: false
      required:
        - total
        - can_move_tasks
      description: Present for column orders. Total includes all non-archived Tasks in the Status, independent of the page and recent cutoff. Permissions are advisory; every mutation reauthorizes.
      properties:
        total:
          type: integer
          format: int64
          minimum: 0
        can_move_tasks:
          type: boolean
    ProjectTaskCounts:
      type: object
      additionalProperties: false
      required:
        - active
        - completed
        - cancelled
        - archived
      properties:
        active:
          type: integer
          format: int64
          minimum: 0
        completed:
          type: integer
          format: int64
          minimum: 0
        cancelled:
          type: integer
          format: int64
          minimum: 0
        archived:
          type: integer
          format: int64
          minimum: 0
    RichTextDocument:
      type: object
      description: |
        Versioned, server-validated rich-text document. The server accepts
        only nodes and marks on the Assign allowlist and rejects anything
        outside it; the client's installed editor extension set is never
        authoritative. Complexity is bounded at 32 levels of nesting and
        50,000 nodes in addition to the per-field byte limits.

        Accepted nodes: `doc`, `paragraph`, `text`, `heading`, `bulletList`,
        `orderedList`, `listItem`, `taskList`, `taskItem`, `blockquote`, `callout`,
        `codeBlock`, `blockMath`, `horizontalRule`, `hardBreak`, `image`, `file`, plus the
        reference nodes `userMention`, `projectReference`, `documentReference`,
        and `taskReference`. Accepted marks: `bold`, `italic`, `underline`,
        `strike`, `code`, `link`.

        A Task-description or Comment mutation accepts at most 100 distinct
        canonical user identifiers across its `userMention` nodes. Repeated
        mentions of the same user count once.

        `image` and `file` name an Assign attachment through
        `attrs.attachmentId` and carry no URL and no bytes: attachments travel
        only through the attachment upload and download operations, and a
        signed URL is minted per request after a fresh authorization check
        rather than stored in content. A request whose `image` or `file` node
        omits `attachmentId`, or carries a URL-bearing attribute, is rejected.

        `callout` has block children and exactly `attrs.kind` and `attrs.icon`.
        Kind is `callout`, `note`, `tip`, `important`, `warning`, or `caution`.
        Icon is null for the default Lucide icon, an emoji of at most 64 UTF-8
        bytes, an allowlisted `lucide:name` token to override it, or an empty string
        to hide it. Unknown Lucide names are rejected. HTML, URLs and image
        data are not accepted as icons. Callouts are additive in schemas 1 and 2.

        `blockMath` stores a required string `attrs.latex`; rendered math HTML
        and renderer settings are not persisted. Schema 1 retains the existing node contract; schema 2 additionally admits tables.
        Checklists, attachment blocks, display equations, `underline`, and
        `projectReference` were added additively, so every
        previously stored document is still valid and no migration is
        required.

        Schema 2 additionally accepts table, tableRow, tableCell and tableHeader.
        A table has tableRow children; a row has tableCell/tableHeader children.
        Cells contain Assign blocks, starting with a paragraph; nested tables
        are rejected. Cells accept colspan/rowspan (integers 1–64, default 1),
        colwidth (null or one integer 24–2000 per spanned column), and align
        (null, left, center or right). Tables/rows have no attrs, marks or text;
        cells have no marks/text or other attributes. The occupancy grid must
        be rectangular with no overlap, holes or spans beyond its rows.
        Limits are 1000 rows, 64 logical columns and 10000 physical cells,
        still subject to each field's byte budget and global node/depth limits.
        Schema 1 rejects all four table nodes. Rich tables exchange through the
        named assign-table Markdown JSON fence; simple tables use GFM pipes.
      required:
        - schema_version
        - type
        - content
      properties:
        schema_version:
          type: integer
          enum:
            - 1
            - 2
          description: Server-validated schema version. Schema 2 table resources cannot be downgraded to schema 1.
        type:
          type: string
          const: doc
        content:
          type: array
          description: Allowlisted block nodes.
          items:
            type: object
      examples:
        - schema_version: 1
          type: doc
          content: []
    Comment:
      type: object
      additionalProperties: false
      description: |
        A Task comment. A deleted comment remains addressable as a tombstone
        with `deleted_at` set and no `body`; its text is cleared at deletion
        rather than merely hidden.
      required:
        - id
        - workspace_id
        - task_id
        - author_actor_id
        - actor
        - actor_name
        - revision
        - created_at
        - updated_at
      properties:
        number:
          type: integer
          format: int64
          minimum: 1
          description: Creation-order ordinal within this Task, including deletion tombstones; use for numbered Comment links.
        id:
          $ref: "#/components/schemas/Identifier"
        workspace_id:
          $ref: "#/components/schemas/Identifier"
        task_id:
          $ref: "#/components/schemas/Identifier"
        author_actor_id:
          $ref: "#/components/schemas/Identifier"
        actor:
          type: string
          enum:
            - user
            - user:delegated_connection
            - user:mcp
            - user:automation
            - user:external_actor
            - agent
            - agent:delegated_connection
            - agent:mcp
            - agent:automation
            - agent:external_actor
            - service
            - service:delegated_connection
            - service:mcp
            - service:automation
            - service:external_actor
          description: |
            Immutable creation-time Actor classification. The base is the
            stable author Actor type; a mediated origin is appended with a
            colon. For example, a user acting through MCP is `user:mcp`.
        actor_name:
          oneOf:
            - type: string
              minLength: 1
              maxLength: 100
            - type: "null"
          description: |
            Presentation-safe client, provider, automation, or external-actor
            name for a compound `actor`; null for a direct base Actor.
        reactions:
          type: array
          maxItems: 6
          description: Non-zero reaction aggregates in the fixed catalog order.
          items:
            $ref: "#/components/schemas/CommentReactionSummary"
        body:
          oneOf:
            - $ref: "#/components/schemas/RichTextDocument"
            - type: "null"
          description: Absent or null once the comment is deleted.
        edited_at:
          oneOf:
            - $ref: "#/components/schemas/Timestamp"
            - type: "null"
          description: Set by the first successful edit. Public, so readers can see the text changed.
        deleted_at:
          oneOf:
            - $ref: "#/components/schemas/Timestamp"
            - type: "null"
        revision:
          type: integer
          minimum: 1
        created_at:
          $ref: "#/components/schemas/Timestamp"
        updated_at:
          $ref: "#/components/schemas/Timestamp"
    CommentReactionKey:
      type: string
      enum:
        - thumbs_up
        - heart
        - tada
        - smile
        - confused
        - eyes
    CommentReactionSummary:
      type: object
      additionalProperties: false
      required:
        - reaction
        - count
        - reacted_by_viewer
      properties:
        reaction:
          $ref: "#/components/schemas/CommentReactionKey"
        count:
          type: integer
          minimum: 1
        reacted_by_viewer:
          type: boolean
    CommentCreateRequest:
      type: object
      additionalProperties: false
      required:
        - body
      properties:
        body:
          $ref: "#/components/schemas/RichTextDocument"
    CommentUpdateRequest:
      type: object
      additionalProperties: false
      required:
        - body
      properties:
        body:
          $ref: "#/components/schemas/RichTextDocument"
    CommentList:
      type: object
      additionalProperties: false
      required:
        - items
        - has_more
      properties:
        total_count:
          type: integer
          format: int64
          minimum: 0
          description: Authorized collection size before cursor bounds; refreshed at read time. Activity counts admitted events before display grouping and Comments include retained ordinals.
        items:
          type: array
          items:
            $ref: "#/components/schemas/Comment"
        next_cursor:
          oneOf:
            - $ref: "#/components/schemas/Cursor"
            - type: "null"
        has_more:
          type: boolean
    PaletteColor:
      type: string
      pattern: ^(slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)(-(50|[1-9]00|950))?$
      maxLength: 11
      description: "Shared user-chosen color: a Tailwind color family name, optionally pinned to one Tailwind step (for example `amber` or `amber-400`). A bare name lets each client choose the shade for its surface; a pinned step renders that exact shade."
      examples:
        - indigo
        - amber-400
    LabelDefinition:
      type: object
      additionalProperties: false
      required:
        - id
        - workspace_id
        - purpose
        - scope
        - name
        - color
        - revision
        - created_at
        - updated_at
      properties:
        id:
          $ref: "#/components/schemas/Identifier"
        workspace_id:
          $ref: "#/components/schemas/Identifier"
        purpose:
          type: string
          enum:
            - document
            - project
            - task
        scope:
          type: string
          enum:
            - workspace
            - project
        project_id:
          oneOf:
            - $ref: "#/components/schemas/Identifier"
            - type: "null"
        name:
          type: string
          minLength: 1
          maxLength: 80
        color:
          $ref: "#/components/schemas/PaletteColor"
        revision:
          type: integer
          minimum: 1
        created_at:
          $ref: "#/components/schemas/Timestamp"
        updated_at:
          $ref: "#/components/schemas/Timestamp"
        archived_at:
          oneOf:
            - $ref: "#/components/schemas/Timestamp"
            - type: "null"
    LabelDefinitionCreateRequest:
      type: object
      additionalProperties: false
      required:
        - purpose
        - scope
        - name
        - color
      properties:
        purpose:
          type: string
          enum:
            - document
            - project
            - task
        scope:
          type: string
          enum:
            - workspace
            - project
        name:
          type: string
          minLength: 1
          maxLength: 80
        color:
          $ref: "#/components/schemas/PaletteColor"
    LabelDefinitionUpdateRequest:
      type: object
      additionalProperties: false
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 80
        color:
          $ref: "#/components/schemas/PaletteColor"
        archived:
          type: boolean
          description: Send false to restore an archived definition. Omit for rename/recolor.
    LabelAssignmentReplaceRequest:
      type: object
      additionalProperties: false
      required:
        - label_ids
      properties:
        label_ids:
          type: array
          maxItems: 20
          items:
            $ref: "#/components/schemas/Identifier"
    LabelDefinitionList:
      type: object
      additionalProperties: false
      required:
        - items
        - has_more
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/LabelDefinition"
        next_cursor:
          oneOf:
            - $ref: "#/components/schemas/Cursor"
            - type: "null"
        has_more:
          type: boolean
    ProjectTaskLabelOverride:
      type: object
      additionalProperties: false
      description: A sparse Project presentation override for a Workspace Task label.
      required:
        - project_id
        - label_id
        - name
        - color
      properties:
        project_id:
          $ref: "#/components/schemas/Identifier"
        label_id:
          $ref: "#/components/schemas/Identifier"
        name:
          type: string
          minLength: 1
          maxLength: 80
        color:
          $ref: "#/components/schemas/PaletteColor"
    ProjectTaskLabelOverrideRequest:
      type: object
      additionalProperties: false
      required:
        - name
        - color
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 80
        color:
          $ref: "#/components/schemas/PaletteColor"
    ProjectTaskLabelOverrideList:
      type: object
      additionalProperties: false
      required:
        - items
        - has_more
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/ProjectTaskLabelOverride"
        next_cursor:
          oneOf:
            - $ref: "#/components/schemas/Cursor"
            - type: "null"
        has_more:
          type: boolean
    Document:
      type: object
      additionalProperties: false
      description: Document metadata. Content is read and replaced through the content operation.
      required:
        - id
        - workspace_id
        - path
        - code
        - title
        - visibility
        - parent_document_id
        - child_document_count
        - public_id
        - created_by_actor_id
        - revision
        - created_at
        - updated_at
        - summary
      properties:
        id:
          $ref: "#/components/schemas/Identifier"
        workspace_id:
          $ref: "#/components/schemas/Identifier"
        path:
          type: string
          pattern: ^[a-z0-9]+(?:[a-z0-9-]*[a-z0-9])?$
          minLength: 1
          maxLength: 63
          description: Permanent Workspace-unique URL segment allocated from the title, with a numeric suffix on collision.
        code:
          $ref: "#/components/schemas/ResourceCode"
        project_id:
          oneOf:
            - $ref: "#/components/schemas/Identifier"
            - type: "null"
        title:
          type: string
          minLength: 1
          maxLength: 500
        visibility:
          type: string
          enum:
            - workspace
            - project
            - public
        parent_document_id:
          oneOf:
            - $ref: "#/components/schemas/Identifier"
            - type: "null"
        child_document_count:
          type: integer
          minimum: 0
        public_id:
          oneOf:
            - $ref: "#/components/schemas/Identifier"
            - type: "null"
        label_ids:
          type: array
          maxItems: 20
          items:
            $ref: "#/components/schemas/Identifier"
        created_by_actor_id:
          $ref: "#/components/schemas/Identifier"
        archived_at:
          oneOf:
            - $ref: "#/components/schemas/Timestamp"
            - type: "null"
        revision:
          type: integer
          minimum: 1
        created_at:
          $ref: "#/components/schemas/Timestamp"
        updated_at:
          $ref: "#/components/schemas/Timestamp"
        summary:
          description: Revision-bound derived convenience text. It is null when the body is shorter than 300 normalized characters, generation is pending, or the stored projection is stale. It is never evidence or a replacement for Document content.
          oneOf:
            - $ref: "#/components/schemas/GeneratedResourceSummary"
            - type: "null"
        ancestors:
          type: array
          maxItems: 32
          description: Ancestor path, root first, present only on rows of a collection page requested with `include_descendants=true` and only for nested Documents.
          items:
            $ref: "#/components/schemas/DocumentAncestor"
    DocumentAncestor:
      type: object
      additionalProperties: false
      description: One step of a Document's ancestor path.
      required:
        - id
        - title
        - path
      properties:
        id:
          $ref: "#/components/schemas/Identifier"
        title:
          type: string
          minLength: 1
          maxLength: 500
        path:
          type: string
          pattern: ^[a-z0-9]+(?:[a-z0-9-]*[a-z0-9])?$
          minLength: 1
          maxLength: 63
    GeneratedResourceSummary:
      type: object
      additionalProperties: false
      description: One- or two-sentence derived display projection for scanning resource collections and detail pages.
      required:
        - text
        - source_revision
        - generated_at
      properties:
        text:
          type: string
          minLength: 1
          maxLength: 400
        source_revision:
          type: integer
          minimum: 1
        generated_at:
          $ref: "#/components/schemas/Timestamp"
    DocumentCreateRequest:
      type: object
      additionalProperties: false
      required:
        - title
      properties:
        title:
          type: string
          minLength: 1
          maxLength: 500
        project_id:
          oneOf:
            - $ref: "#/components/schemas/Identifier"
            - type: "null"
        visibility:
          type: string
          enum:
            - workspace
            - project
            - public
        parent_document_id:
          oneOf:
            - $ref: "#/components/schemas/Identifier"
            - type: "null"
        content:
          $ref: "#/components/schemas/RichTextDocument"
    DocumentUpdateRequest:
      type: object
      additionalProperties: false
      description: Every field is optional; an omitted field keeps its current value.
      properties:
        title:
          type: string
          minLength: 1
          maxLength: 500
        project_id:
          oneOf:
            - $ref: "#/components/schemas/Identifier"
            - type: "null"
        visibility:
          type: string
          enum:
            - workspace
            - project
            - public
        parent_document_id:
          oneOf:
            - $ref: "#/components/schemas/Identifier"
            - type: "null"
    DocumentContent:
      type: object
      additionalProperties: false
      required:
        - document_id
        - revision
        - content
        - updated_at
      properties:
        document_id:
          $ref: "#/components/schemas/Identifier"
        revision:
          type: integer
          minimum: 1
          description: The revision this content belongs to. Send it back in `If-Match` to save.
        content:
          $ref: "#/components/schemas/RichTextDocument"
        updated_at:
          $ref: "#/components/schemas/Timestamp"
    DocumentContentReplaceRequest:
      type: object
      additionalProperties: false
      required:
        - content
      properties:
        content:
          $ref: "#/components/schemas/RichTextDocument"
    DocumentRevision:
      type: object
      additionalProperties: false
      required:
        - document_id
        - revision
        - actor_id
        - change_kind
        - project_id
        - title
        - path
        - content
        - visibility
        - parent_document_id
        - archived_at
        - created_at
      properties:
        document_id:
          $ref: "#/components/schemas/Identifier"
        revision:
          type: integer
          minimum: 1
        actor_id:
          oneOf:
            - $ref: "#/components/schemas/Identifier"
            - type: "null"
        change_kind:
          type: string
          enum:
            - baseline
            - created
            - metadata
            - content
            - collaboration_checkpoint
            - archived
            - restored
        project_id:
          oneOf:
            - $ref: "#/components/schemas/Identifier"
            - type: "null"
        title:
          type: string
          minLength: 1
          maxLength: 500
        path:
          type: string
          minLength: 1
          maxLength: 2000
        content:
          $ref: "#/components/schemas/RichTextDocument"
        visibility:
          type: string
          enum:
            - workspace
            - project
            - public
        parent_document_id:
          oneOf:
            - $ref: "#/components/schemas/Identifier"
            - type: "null"
        archived_at:
          oneOf:
            - $ref: "#/components/schemas/Timestamp"
            - type: "null"
        created_at:
          $ref: "#/components/schemas/Timestamp"
    DocumentRevisionList:
      type: object
      additionalProperties: false
      required:
        - items
        - next_cursor
        - has_more
      properties:
        items:
          type: array
          maxItems: 100
          items:
            $ref: "#/components/schemas/DocumentRevision"
        next_cursor:
          oneOf:
            - $ref: "#/components/schemas/Cursor"
            - type: "null"
        has_more:
          type: boolean
    DocumentRevisionComparison:
      type: object
      additionalProperties: false
      required:
        - from
        - to
        - changed_fields
      properties:
        from:
          $ref: "#/components/schemas/DocumentRevision"
        to:
          $ref: "#/components/schemas/DocumentRevision"
        changed_fields:
          type: array
          uniqueItems: true
          items:
            type: string
            enum:
              - title
              - path
              - project_id
              - visibility
              - parent_document_id
              - content
              - archived
    DocumentCollaborationSession:
      type: object
      additionalProperties: false
      description: |
        Protocol-independent Document admission and ephemeral presence lease.
        `admission_token` is a short-lived opaque value for a future
        synchronization service; it is never an HTTP credential.
      required:
        - id
        - role
        - admission_token
        - expires_at
        - heartbeat_interval_seconds
      properties:
        generation:
          type: string
          format: uuid
          description: Task replica generation; an ordinary description replacement invalidates retained updates from the old generation.
        id:
          $ref: "#/components/schemas/Identifier"
        role:
          type: string
          enum:
            - editor
            - viewer
        admission_token:
          type: string
          minLength: 43
          maxLength: 43
          readOnly: true
        expires_at:
          $ref: "#/components/schemas/Timestamp"
        heartbeat_interval_seconds:
          type: integer
          minimum: 1
          description: Send another admission request within this interval to retain presence.
    ReferenceOptionType:
      type: string
      enum:
        - user
        - agent
        - document
        - project
        - task
    ReferenceOption:
      type: object
      additionalProperties: false
      required:
        - type
        - id
        - label
        - uri
      properties:
        type:
          $ref: "#/components/schemas/ReferenceOptionType"
        id:
          $ref: "#/components/schemas/Identifier"
        label:
          type: string
          minLength: 1
          maxLength: 500
        secondary_label:
          oneOf:
            - type: string
              minLength: 1
              maxLength: 500
            - type: "null"
        uri:
          type: string
          pattern: ^assign:(user|agent|document|project|task)/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$
          description: Deployment-independent reference URI stored by the editor.
    ReferenceOptionList:
      type: object
      additionalProperties: false
      required:
        - users
        - agents
        - documents
        - projects
        - tasks
      properties:
        users:
          type: array
          maxItems: 20
          items:
            $ref: "#/components/schemas/ReferenceOption"
        agents:
          type: array
          maxItems: 20
          items:
            $ref: "#/components/schemas/ReferenceOption"
        documents:
          type: array
          maxItems: 20
          items:
            $ref: "#/components/schemas/ReferenceOption"
        projects:
          type: array
          maxItems: 20
          items:
            $ref: "#/components/schemas/ReferenceOption"
        tasks:
          type: array
          maxItems: 20
          items:
            $ref: "#/components/schemas/ReferenceOption"
    PublicDocument:
      type: object
      additionalProperties: false
      description: Anonymous, public-safe representation of a published Document.
      required:
        - public_id
        - title
        - content
        - updated_at
      properties:
        public_id:
          $ref: "#/components/schemas/Identifier"
        title:
          type: string
          minLength: 1
          maxLength: 500
        content:
          $ref: "#/components/schemas/RichTextDocument"
        updated_at:
          $ref: "#/components/schemas/Timestamp"
    DocumentContentConflictResponse:
      type: object
      additionalProperties: false
      description: |
        A stale save. Carries the current server content so the client can
        compare against its local draft without a second request.
      required:
        - error
        - current
      properties:
        error:
          $ref: "#/components/schemas/Error"
        current:
          $ref: "#/components/schemas/DocumentContent"
    DocumentList:
      type: object
      additionalProperties: false
      required:
        - items
        - has_more
      properties:
        total_count:
          type: integer
          format: int64
          minimum: 0
          description: Authorized collection size before cursor bounds; refreshed at read time. Activity counts admitted events before display grouping and Comments include retained ordinals.
        items:
          type: array
          items:
            $ref: "#/components/schemas/Document"
        next_cursor:
          oneOf:
            - $ref: "#/components/schemas/Cursor"
            - type: "null"
        has_more:
          type: boolean
    Password:
      type: string
      minLength: 12
      maxLength: 128
      format: password
      description: |
        Between 12 and 128 characters. There are deliberately no composition
        rules: length and non-reuse resist guessing, while character-class
        rules mostly produce predictable substitutions. Passwords appearing in
        a known breach corpus are refused.
    EmailAddress:
      type: string
      format: email
      minLength: 3
      maxLength: 320
      description: Normalized to lowercase and trimmed before storage and comparison.
    SingleUseToken:
      type: string
      minLength: 1
      maxLength: 512
      description: |
        A single-use, expiring bearer secret delivered by email. Only its hash
        is stored, so it cannot be recovered from a database disclosure. Treat
        it as a credential: never log it or place it in a URL you retain.
    RegisterRequest:
      type: object
      additionalProperties: false
      required:
        - email
        - password
      properties:
        email:
          $ref: "#/components/schemas/EmailAddress"
        password:
          $ref: "#/components/schemas/Password"
        display_name:
          type: string
          minLength: 1
          maxLength: 100
          description: |
            Optional. When omitted, a starting name is derived from the local
            part of the address; it is a default the user can change, not an
            identity claim.
        invitation_token:
          type: string
          minLength: 1
          maxLength: 512
          writeOnly: true
          description: |
            Optional pending Workspace invitation credential delivered to this
            email address. Proves mailbox control without a separate verification
            email. Does not consume the invitation or grant membership.
    LoginRequest:
      type: object
      additionalProperties: false
      required:
        - email
        - password
      properties:
        email:
          $ref: "#/components/schemas/EmailAddress"
        password:
          $ref: "#/components/schemas/Password"
        remember_me:
          type: boolean
          description: |
            Whether to remember this device. `true` (the default when omitted,
            which preserves earlier client behavior) issues a persistent
            session cookie with the 7-day idle and 30-day absolute lifetimes.
            `false` issues a browser-session cookie that the browser discards
            when it closes, and the server bounds the session to 48 hours,
            idle and absolute.
    EmailRequest:
      type: object
      additionalProperties: false
      required:
        - email
      properties:
        email:
          $ref: "#/components/schemas/EmailAddress"
    TokenRequest:
      type: object
      additionalProperties: false
      required:
        - token
      properties:
        token:
          $ref: "#/components/schemas/SingleUseToken"
    PasswordResetRequest:
      type: object
      additionalProperties: false
      required:
        - token
        - password
      properties:
        token:
          $ref: "#/components/schemas/SingleUseToken"
        password:
          $ref: "#/components/schemas/Password"
    PasswordChangeRequest:
      type: object
      additionalProperties: false
      required:
        - current_password
        - new_password
      properties:
        current_password:
          type: string
          minLength: 1
          maxLength: 128
          format: password
        new_password:
          $ref: "#/components/schemas/Password"
    AuthenticatedAccount:
      type: object
      additionalProperties: false
      description: |
        The signed-in user and, when selected, the Workspace their session is
        bound to. A newly registered account with no Workspace has none of the
        Workspace fields. Selecting or creating a Workspace rotates both
        session secrets.
      required:
        - user_id
        - email
        - display_name
      properties:
        user_id:
          $ref: "#/components/schemas/Identifier"
        email:
          $ref: "#/components/schemas/EmailAddress"
        display_name:
          type: string
        workspace_id:
          $ref: "#/components/schemas/Identifier"
        workspace_name:
          type: string
        workspace_slug:
          type: string
        role:
          type: string
    LoginResult:
      description: |
        The two outcomes of a password sign-in. Branch on `mfa_required`: it is
        absent on a completed sign-in and `true` on a challenge. A client that
        assumes the account body is present will misread a challenge as a
        session.
      oneOf:
        - $ref: "#/components/schemas/AuthenticatedAccount"
        - $ref: "#/components/schemas/MFARequired"
    MFARequired:
      type: object
      additionalProperties: false
      description: |
        A half-finished sign-in. It deliberately carries no account or
        Workspace data: the password is proven and nothing else, and the
        challenge must not be usable to learn who the account belongs to.
      required:
        - mfa_required
        - mfa_token
        - expires_at
      properties:
        mfa_required:
          type: boolean
          description: |
            Always `true`. Its **presence** is what distinguishes this body
            from a completed sign-in, which omits the field entirely — branch
            on whether the field is there, not on its value.

            It is not constrained to a constant here because generators render
            a single-valued boolean enum as an invalid enumeration; the two
            branches stay unambiguous through this field being required on
            this schema and rejected by the other.
        mfa_token:
          type: string
          description: |
            The single-use challenge to present to `verifyMFA`. Hold it in
            memory for the duration of the sign-in; it is a credential.
          minLength: 1
          maxLength: 128
        expires_at:
          type: string
          format: date-time
          description: |
            When the challenge stops being exchangeable. Roughly five minutes
            out; after it passes the user must sign in again.
    MFAVerifyRequest:
      type: object
      additionalProperties: false
      required:
        - mfa_token
        - code
      properties:
        mfa_token:
          type: string
          minLength: 1
          maxLength: 128
        code:
          $ref: "#/components/schemas/MFACode"
        remember_me:
          type: boolean
          description: |
            Whether to remember this device; use the value chosen at the first sign-in step. `true` (the default when omitted,
            which preserves earlier client behavior) issues a persistent
            session cookie with the 7-day idle and 30-day absolute lifetimes.
            `false` issues a browser-session cookie that the browser discards
            when it closes, and the server bounds the session to 48 hours,
            idle and absolute.
    MFACodeRequest:
      type: object
      additionalProperties: false
      required:
        - code
      properties:
        code:
          $ref: "#/components/schemas/MFACode"
    MFADisableRequest:
      type: object
      additionalProperties: false
      required:
        - current_password
        - code
      properties:
        current_password:
          type: string
          minLength: 1
          maxLength: 128
          format: password
        code:
          $ref: "#/components/schemas/MFACode"
    MFARegenerateRequest:
      type: object
      additionalProperties: false
      required:
        - current_password
      properties:
        current_password:
          type: string
          minLength: 1
          maxLength: 128
          format: password
    MFACode:
      type: string
      description: |
        Either a six-digit TOTP code or one of the account's recovery codes.
        Recovery codes are matched after stripping spaces and hyphens and
        upper-casing, so a code read off paper matches however it was grouped.
      minLength: 1
      maxLength: 64
      format: password
    TOTPSetup:
      type: object
      additionalProperties: false
      description: |
        The enrolment material, returned once and never again. Present the URI
        as a QR code and the secret as the manual-entry fallback.
      required:
        - secret
        - provisioning_uri
      properties:
        secret:
          type: string
          description: |
            The shared secret in unpadded base32, the form authenticator apps
            accept when it is typed in by hand.
          format: password
        provisioning_uri:
          type: string
          format: uri
          description: |
            An `otpauth://totp/` URI carrying the secret, issuer, algorithm,
            digit count, and period.
    RecoveryCodes:
      type: object
      additionalProperties: false
      description: |
        Single-use codes that stand in for the authenticator when it is lost.
        Returned once; only hashes are stored. Tell the user to save them
        before dismissing the response.
      required:
        - recovery_codes
      properties:
        recovery_codes:
          type: array
          minItems: 10
          maxItems: 10
          items:
            type: string
            format: password
    PasskeyCeremony:
      type: object
      additionalProperties: false
      description: |
        A started WebAuthn ceremony. Hold the token for the duration and return
        it with the authenticator's response; it is single-use and bound to the
        ceremony it started.
      required:
        - ceremony_token
        - expires_at
        - options
      properties:
        ceremony_token:
          type: string
          description: |
            The single-use handle for this ceremony. It is a credential — do
            not log it or place it in a URL.
          minLength: 1
          maxLength: 128
        expires_at:
          type: string
          format: date-time
          description: |
            When the ceremony stops being completable. Roughly five minutes
            out; after it passes, start a new one.
        options:
          type: object
          additionalProperties: true
          description: |
            The WebAuthn options structure, passed to the browser **unchanged**.
            It is deliberately untyped here: it is defined by the WebAuthn
            specification rather than by this API, and restating it would create
            a second copy to keep in step. Give it to
            `navigator.credentials.create()` or `.get()` after the usual
            base64url decoding a WebAuthn client library performs.
    PasskeyAttestationRequest:
      type: object
      additionalProperties: false
      required:
        - ceremony_token
        - name
        - response
      properties:
        ceremony_token:
          type: string
          minLength: 1
          maxLength: 128
        name:
          $ref: "#/components/schemas/PasskeyName"
        response:
          type: object
          additionalProperties: true
          description: |
            The `PublicKeyCredential` returned by
            `navigator.credentials.create()`, serialized as JSON in the form a
            WebAuthn client library produces.
    PasskeyAssertionRequest:
      type: object
      additionalProperties: false
      required:
        - ceremony_token
        - response
      properties:
        ceremony_token:
          type: string
          minLength: 1
          maxLength: 128
        response:
          type: object
          additionalProperties: true
          description: |
            The `PublicKeyCredential` returned by
            `navigator.credentials.get()`, serialized as JSON in the form a
            WebAuthn client library produces.
        remember_me:
          type: boolean
          description: |
            Whether to remember this device. `true` (the default when omitted,
            which preserves earlier client behavior) issues a persistent
            session cookie with the 7-day idle and 30-day absolute lifetimes.
            `false` issues a browser-session cookie that the browser discards
            when it closes, and the server bounds the session to 48 hours,
            idle and absolute.
    PasskeyRenameRequest:
      type: object
      additionalProperties: false
      required:
        - name
      properties:
        name:
          $ref: "#/components/schemas/PasskeyName"
    PasskeyName:
      type: string
      description: |
        The user's own label for the device, shown in their passkey list.
        Surrounding whitespace is trimmed; control characters are rejected.
      minLength: 1
      maxLength: 64
    Passkey:
      type: object
      additionalProperties: false
      description: |
        One registered passkey. The public key and attestation data are not
        included — a client has no use for them, and the identifier is enough to
        manage the credential.
      required:
        - id
        - name
        - backup_eligible
        - backup_state
        - disabled
        - created_at
        - last_used_at
      properties:
        id:
          $ref: "#/components/schemas/Identifier"
        name:
          $ref: "#/components/schemas/PasskeyName"
        backup_eligible:
          type: boolean
          description: |
            Whether the authenticator reported the credential as syncable. This
            never changes for a credential. Together with `backup_state` it is
            what tells a user whether losing the device means losing the
            passkey.
        backup_state:
          type: boolean
          description: Whether the credential is currently backed up or synced.
        disabled:
          type: boolean
          description: |
            `true` when the credential was disabled because its signature
            counter failed to advance, which indicates a second copy of the
            private key. It cannot be used to sign in and cannot be
            re-enabled — remove it and register a new one.
        created_at:
          type: string
          format: date-time
        last_used_at:
          description: Null until the passkey has been used to sign in.
          oneOf:
            - type: string
              format: date-time
            - type: "null"
    PasskeyList:
      type: object
      additionalProperties: false
      required:
        - passkeys
      properties:
        passkeys:
          type: array
          maxItems: 10
          items:
            $ref: "#/components/schemas/Passkey"
    AttachmentScanState:
      type: string
      description: |
        Malware-scan state. New uploads are scanned synchronously before
        they become usable: only `clean` attachments may be linked, previewed,
        or downloaded. `quarantined` attachments remain private and unavailable.
        `not_scanned` is retained only for legacy migration visibility.
      enum:
        - not_scanned
        - pending
        - clean
        - quarantined
    AttachmentUploadRequest:
      type: object
      additionalProperties: false
      required:
        - workspace_id
        - filename
        - content_length
      properties:
        workspace_id:
          $ref: "#/components/schemas/Identifier"
        filename:
          $ref: "#/components/schemas/AttachmentFilename"
        content_length:
          type: integer
          minimum: 1
          maximum: 52428800
          description: Exact byte count to reserve. The free tier allows 50 MiB per object.
        content_type:
          type: string
          maxLength: 255
          description: |
            Advisory only. The stored content type is detected from the
            object's leading bytes at completion and may differ.
        sha256:
          $ref: "#/components/schemas/Sha256Digest"
    AttachmentFilename:
      type: string
      minLength: 1
      maxLength: 255
      description: |
        Display filename, at most 255 UTF-8 bytes after NFC normalization.
        It must not contain a path separator, null, or control character,
        must not be `.` or `..`, and must not begin or end with whitespace or
        a dot. A violating filename is rejected rather than rewritten.
        Filenames never contribute to the server-generated object key.
    Sha256Digest:
      type: string
      pattern: ^[0-9a-f]{64}$
      description: Lowercase hexadecimal SHA-256 digest.
    AttachmentUpload:
      type: object
      additionalProperties: false
      required:
        - upload_id
        - upload_url
        - method
        - expires_at
        - content_length
      properties:
        upload_id:
          $ref: "#/components/schemas/Identifier"
        upload_url:
          type: string
          format: uri
          description: |
            Short-lived presigned URL bound to one method, one object key,
            and the reserved length. Treat it as a credential: never log it,
            store it, or share it.
        method:
          type: string
          enum:
            - PUT
        required_headers:
          type: object
          additionalProperties:
            type: string
          description: Headers that must accompany the upload request exactly as given.
        expires_at:
          $ref: "#/components/schemas/Timestamp"
        content_length:
          type: integer
          minimum: 1
    AttachmentUploadCompleteRequest:
      type: object
      additionalProperties: false
      properties:
        sha256:
          $ref: "#/components/schemas/Sha256Digest"
    Attachment:
      type: object
      additionalProperties: false
      required:
        - id
        - workspace_id
        - filename
        - content_length
        - content_type
        - scan_state
        - created_by_actor_id
        - created_at
      properties:
        id:
          $ref: "#/components/schemas/Identifier"
        workspace_id:
          $ref: "#/components/schemas/Identifier"
        filename:
          $ref: "#/components/schemas/AttachmentFilename"
        content_length:
          type: integer
          minimum: 1
        content_type:
          type: string
          maxLength: 255
          description: Server-detected from the object's leading bytes, not client-declared.
        scan_state:
          $ref: "#/components/schemas/AttachmentScanState"
        created_by_actor_id:
          $ref: "#/components/schemas/Identifier"
        deleted_at:
          oneOf:
            - $ref: "#/components/schemas/Timestamp"
            - type: "null"
        created_at:
          $ref: "#/components/schemas/Timestamp"
    AttachmentDownload:
      type: object
      additionalProperties: false
      required:
        - download_url
        - expires_at
      properties:
        download_url:
          type: string
          format: uri
          description: |
            Short-lived presigned URL bound to one method and one object key.
            Treat it as a credential: never log, store, or embed it.
        expires_at:
          $ref: "#/components/schemas/Timestamp"
    AttachmentPreview:
      type: object
      additionalProperties: false
      required:
        - preview_url
        - content_type
        - expires_at
      properties:
        preview_url:
          type: string
          format: uri
          description: |
            Short-lived presigned inline URL bound to one method and one object
            key. Treat it as a credential: never log, store, or embed it.
        content_type:
          type: string
          enum:
            - application/pdf
            - image/avif
            - image/gif
            - image/jpeg
            - image/png
            - image/webp
          description: Server-detected renderable content type.
        expires_at:
          $ref: "#/components/schemas/Timestamp"
    AttachmentLinkRequest:
      type: object
      additionalProperties: false
      required:
        - attachment_id
      properties:
        attachment_id:
          $ref: "#/components/schemas/Identifier"
    AttachmentList:
      type: object
      additionalProperties: false
      required:
        - items
      properties:
        items:
          type: array
          maxItems: 100
          items:
            $ref: "#/components/schemas/Attachment"
    StorageQuota:
      type: object
      additionalProperties: false
      description: |
        Storage accounting at the moment the request was refused. Entitlement
        is pooled across every Workspace owned by the subscription.
      required:
        - limit_bytes
        - used_bytes
        - per_object_limit_bytes
      properties:
        limit_bytes:
          type: integer
          minimum: 0
        used_bytes:
          type: integer
          minimum: 0
        per_object_limit_bytes:
          type: integer
          minimum: 1
        upgradeable:
          type: boolean
          description: Whether upgrading the plan can resolve the refusal.
    QuotaExceededResponse:
      type: object
      additionalProperties: false
      required:
        - error
        - quota
      properties:
        error:
          $ref: "#/components/schemas/Error"
        quota:
          $ref: "#/components/schemas/StorageQuota"
    ActivityGroupCount:
      type: object
      additionalProperties: false
      required:
        - value
        - accuracy
        - resource_kind
      properties:
        value:
          type:
            - integer
            - "null"
          format: int64
          minimum: 0
        accuracy:
          type: string
          maxLength: 128
          enum:
            - exact
            - lower_bound
            - unknown
        resource_kind:
          type: string
          maxLength: 128
    ActivityGroupCounts:
      type: object
      additionalProperties: false
      required:
        - projects
        - distinct_resources
        - occurrences
        - children
      properties:
        projects:
          $ref: "#/components/schemas/ActivityGroupCount"
        distinct_resources:
          $ref: "#/components/schemas/ActivityGroupCount"
        occurrences:
          $ref: "#/components/schemas/ActivityGroupCount"
        children:
          $ref: "#/components/schemas/ActivityGroupCount"
    ActivityGroupCollectionCounts:
      type: object
      additionalProperties: false
      required:
        - projects
        - groups
        - distinct_resources
        - occurrences
      properties:
        projects:
          $ref: "#/components/schemas/ActivityGroupCount"
        groups:
          $ref: "#/components/schemas/ActivityGroupCount"
        distinct_resources:
          $ref: "#/components/schemas/ActivityGroupCount"
        occurrences:
          $ref: "#/components/schemas/ActivityGroupCount"
    ActivityGroupCoverage:
      type: object
      additionalProperties: false
      required:
        - status
        - reasons
        - retained_days
        - retained_from
        - history_before_retention
      properties:
        status:
          type: string
          maxLength: 128
          enum:
            - complete
            - partial
            - stale
        reasons:
          type: array
          maxItems: 50
          items:
            type: string
            maxLength: 4096
        retained_days:
          type: integer
          enum:
            - 90
        retained_from:
          type: string
          format: date-time
        history_before_retention:
          type: string
          maxLength: 128
          enum:
            - unknown
    ActivityGroupActor:
      type: object
      additionalProperties: false
      required:
        - id
        - display_name
        - automation
        - profile_link
        - identity_state
      properties:
        id:
          type:
            - string
            - "null"
          format: uuid
        display_name:
          type: string
          maxLength: 400
        automation:
          type: boolean
        profile_link:
          type:
            - string
            - "null"
          maxLength: 1024
        identity_state:
          type: string
          maxLength: 128
    ActivityGroupProvenance:
      type: object
      additionalProperties: false
      required:
        - kind
        - name
        - source_key
      properties:
        kind:
          type: string
          maxLength: 128
        name:
          type: string
          maxLength: 256
        source_key:
          type: string
          maxLength: 128
    ActivityGroupReference:
      type: object
      additionalProperties: false
      required:
        - role
        - kind
        - state
        - id
        - label
        - link
      properties:
        role:
          type: string
          maxLength: 128
        kind:
          type: string
          maxLength: 128
        state:
          type: string
          maxLength: 128
          enum:
            - available
            - deleted
            - unavailable
        id:
          type:
            - string
            - "null"
          format: uuid
        label:
          type:
            - string
            - "null"
          maxLength: 256
        link:
          type:
            - string
            - "null"
          maxLength: 1024
    ActivityGroupValue:
      type: object
      additionalProperties: false
      required:
        - state
      properties:
        state:
          type: string
          maxLength: 128
          enum:
            - known
            - empty
            - unknown
            - unavailable
        value:
          oneOf:
            - type: string
              maxLength: 256
            - type: boolean
            - type: number
            - $ref: "#/components/schemas/ActivityGroupReference"
    ActivityGroupFact:
      type: object
      additionalProperties: false
      required:
        - field
        - value_type
        - before
        - after
      properties:
        field:
          type: string
          maxLength: 128
        value_type:
          type: string
          maxLength: 128
        before:
          $ref: "#/components/schemas/ActivityGroupValue"
        after:
          $ref: "#/components/schemas/ActivityGroupValue"
    ActivityGroupExpansion:
      type: object
      additionalProperties: false
      required:
        - available
        - coverage
        - first_cursor
      properties:
        available:
          type: boolean
        coverage:
          $ref: "#/components/schemas/ActivityGroupCoverage"
        first_cursor:
          type:
            - string
            - "null"
          maxLength: 4096
    ActivityGroupView:
      type: object
      additionalProperties: false
      required:
        - id
        - revision
        - kind
        - outcome
        - icon
        - importance
        - day
        - first_occurred_at
        - last_occurred_at
        - actor
        - provenance
        - counts
        - facts
        - references
        - preview_keys
        - expansion
      properties:
        id:
          type: string
          maxLength: 192
        revision:
          type: string
          maxLength: 128
        kind:
          type: string
          maxLength: 128
        outcome:
          type: string
          maxLength: 128
        icon:
          type: string
          maxLength: 128
        importance:
          type: string
          maxLength: 128
        day:
          type: string
          format: date
          maxLength: 10
        first_occurred_at:
          type: string
          format: date-time
        last_occurred_at:
          type: string
          format: date-time
        actor:
          $ref: "#/components/schemas/ActivityGroupActor"
        provenance:
          anyOf:
            - $ref: "#/components/schemas/ActivityGroupProvenance"
            - type: "null"
        counts:
          $ref: "#/components/schemas/ActivityGroupCounts"
        facts:
          type: array
          maxItems: 16
          items:
            $ref: "#/components/schemas/ActivityGroupFact"
        references:
          type: array
          maxItems: 8
          items:
            $ref: "#/components/schemas/ActivityGroupReference"
        preview_keys:
          type: array
          maxItems: 3
          items:
            type: string
            maxLength: 4096
        expansion:
          $ref: "#/components/schemas/ActivityGroupExpansion"
    ActivityGroupPreview:
      type: object
      additionalProperties: false
      required:
        - key
        - kind
        - reference
        - state
        - observed_at
        - resource_revision
        - title
        - metadata
        - excerpt
      properties:
        key:
          type: string
          maxLength: 128
        kind:
          type: string
          maxLength: 128
        reference:
          $ref: "#/components/schemas/ActivityGroupReference"
        state:
          type: string
          maxLength: 128
        observed_at:
          type: string
          format: date-time
        resource_revision:
          type:
            - string
            - "null"
          maxLength: 128
        title:
          type:
            - string
            - "null"
          maxLength: 256
        metadata:
          type: array
          maxItems: 16
          items:
            $ref: "#/components/schemas/ActivityGroupFact"
        excerpt:
          type:
            - string
            - "null"
          maxLength: 140
    ActivityGroupPage:
      type: object
      additionalProperties: false
      required:
        - snapshot_id
        - schema
        - policy_version
        - scope
        - resource_id
        - time_zone
        - from
        - before
        - observed_at
        - snapshot
        - items
        - counts
        - coverage
        - has_more
        - next_cursor
        - previews
      properties:
        snapshot_id:
          type: string
          maxLength: 128
        schema:
          type: string
          maxLength: 128
          enum:
            - assign.activity.groups.v1
        policy_version:
          type: string
          maxLength: 128
        scope:
          type: string
          maxLength: 128
          enum:
            - workspace
            - account
            - person
            - project
            - task
        resource_id:
          type:
            - string
            - "null"
          format: uuid
        time_zone:
          type: string
          maxLength: 128
        from:
          type: string
          format: date-time
        before:
          type: string
          format: date-time
        observed_at:
          type: string
          format: date-time
        snapshot:
          type: string
          maxLength: 4096
        items:
          type: array
          maxItems: 50
          items:
            $ref: "#/components/schemas/ActivityGroupView"
        counts:
          $ref: "#/components/schemas/ActivityGroupCollectionCounts"
        coverage:
          $ref: "#/components/schemas/ActivityGroupCoverage"
        has_more:
          type: boolean
        next_cursor:
          type:
            - string
            - "null"
          maxLength: 4096
        previews:
          type: array
          maxItems: 150
          items:
            $ref: "#/components/schemas/ActivityGroupPreview"
    ActivityGroupChildSection:
      type: object
      additionalProperties: false
      required:
        - id
        - first_occurred_at
        - last_occurred_at
        - continues_before
        - continues_after
        - project_reference
      properties:
        id:
          type: string
          maxLength: 128
        first_occurred_at:
          type: string
          format: date-time
        last_occurred_at:
          type: string
          format: date-time
        continues_before:
          type: boolean
        continues_after:
          type: boolean
        project_reference:
          anyOf:
            - $ref: "#/components/schemas/ActivityGroupReference"
            - type: "null"
    ActivityGroupChildView:
      type: object
      additionalProperties: false
      required:
        - parent_id
        - id
        - revision
        - occurred_at
        - first_occurred_at
        - last_occurred_at
        - outcome
        - operation_id
        - actor
        - provenance
        - facts
        - references
        - counts
        - preview_keys
        - section
      properties:
        parent_id:
          type:
            - string
            - "null"
          maxLength: 128
        id:
          type: string
          maxLength: 128
        revision:
          type: string
          maxLength: 128
        occurred_at:
          type: string
          format: date-time
        first_occurred_at:
          type: string
          format: date-time
        last_occurred_at:
          type: string
          format: date-time
        outcome:
          type: string
          maxLength: 128
        operation_id:
          type:
            - string
            - "null"
          format: uuid
        actor:
          $ref: "#/components/schemas/ActivityGroupActor"
        provenance:
          anyOf:
            - $ref: "#/components/schemas/ActivityGroupProvenance"
            - type: "null"
        facts:
          type: array
          maxItems: 16
          items:
            $ref: "#/components/schemas/ActivityGroupFact"
        references:
          type: array
          maxItems: 8
          items:
            $ref: "#/components/schemas/ActivityGroupReference"
        counts:
          $ref: "#/components/schemas/ActivityGroupCounts"
        preview_keys:
          type: array
          maxItems: 3
          items:
            type: string
            maxLength: 4096
        section:
          $ref: "#/components/schemas/ActivityGroupChildSection"
    ActivityGroupChildPage:
      type: object
      additionalProperties: false
      required:
        - snapshot_id
        - schema
        - policy_version
        - group_id
        - group_revision
        - snapshot
        - observed_at
        - items
        - counts
        - coverage
        - has_more
        - next_cursor
        - previews
      properties:
        snapshot_id:
          type: string
          maxLength: 128
        schema:
          type: string
          maxLength: 128
          enum:
            - assign.activity.children.v1
        policy_version:
          type: string
          maxLength: 128
        group_id:
          type: string
          maxLength: 192
        group_revision:
          type: string
          maxLength: 128
        snapshot:
          type: string
          maxLength: 4096
        observed_at:
          type: string
          format: date-time
        items:
          type: array
          maxItems: 25
          items:
            $ref: "#/components/schemas/ActivityGroupChildView"
        counts:
          $ref: "#/components/schemas/ActivityGroupCounts"
        coverage:
          $ref: "#/components/schemas/ActivityGroupCoverage"
        has_more:
          type: boolean
        next_cursor:
          type:
            - string
            - "null"
          maxLength: 4096
        previews:
          type: array
          maxItems: 150
          items:
            $ref: "#/components/schemas/ActivityGroupPreview"
  responses:
    DocumentSchemaUnsupported:
      description: "document_schema_version_unsupported: the client cannot write the current resource schema, or the mutation attempts to downgrade a schema 2 resource. Update to a client supporting schema 2 and obtain a new admission."
      headers:
        Cache-Control:
          $ref: "#/components/headers/CacheControlNoStore"
        X-Request-ID:
          $ref: "#/components/headers/RequestId"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    WorkspaceInvitationUnavailable:
      description: |
        Invitation creation is temporarily unavailable. The standard error
        envelope has code `delivery_unavailable` when the configured email
        provider cannot accept the message; no invitation is retained in that
        case. Scheduled maintenance may instead return `maintenance_offline`
        or `maintenance_read_only`. Clients should keep the entered invitation
        details and allow a later retry.
      headers:
        Retry-After:
          description: Delay in seconds before retrying, when known.
          schema:
            type: integer
            minimum: 0
        Cache-Control:
          $ref: "#/components/headers/CacheControlNoStore"
        X-Request-ID:
          $ref: "#/components/headers/RequestId"
        X-Content-Type-Options:
          $ref: "#/components/headers/ContentTypeOptions"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    MaintenanceUnavailable:
      description: |
        Scheduled maintenance temporarily prevents this operation. The standard
        error envelope has code `maintenance_offline` when the service is
        unavailable. Mutating operations may instead return
        `maintenance_read_only` while reads remain available. Clients must
        treat both codes as retryable availability states, not authorization
        failures, and honor `Retry-After` when present.
      headers:
        Retry-After:
          description: Delay in seconds before retrying, when an expected maintenance end is known.
          schema:
            type: integer
            minimum: 0
        Cache-Control:
          $ref: "#/components/headers/CacheControlNoStore"
        X-Request-ID:
          $ref: "#/components/headers/RequestId"
        X-Content-Type-Options:
          $ref: "#/components/headers/ContentTypeOptions"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    PasswordRejected:
      description: |
        The request was malformed, or the supplied password was refused by
        policy. Policy refusals are distinguishable — `password_too_short`,
        `password_too_long`, `password_compromised` — because the user is
        choosing a value and cannot correct it from a generic message. None of
        these codes reveal whether an account exists.
      headers:
        Cache-Control:
          $ref: "#/components/headers/CacheControlNoStore"
        X-Request-ID:
          $ref: "#/components/headers/RequestId"
        X-Content-Type-Options:
          $ref: "#/components/headers/ContentTypeOptions"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    PayloadTooLarge:
      description: |
        The request body exceeds the limit for this field or operation.
        Document JSON is capped at 2 MiB, a single comment at 256 KiB, and a
        generic JSON request at 1 MiB.
      headers:
        Cache-Control:
          $ref: "#/components/headers/CacheControlNoStore"
        X-Request-ID:
          $ref: "#/components/headers/RequestId"
        X-Content-Type-Options:
          $ref: "#/components/headers/ContentTypeOptions"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    DocumentContentConflict:
      description: |
        The supplied revision is stale. The body carries the current server
        content so the client can compare, reload, or retry without losing
        its local draft.
      headers:
        Cache-Control:
          $ref: "#/components/headers/CacheControlNoStore"
        X-Request-ID:
          $ref: "#/components/headers/RequestId"
        X-Content-Type-Options:
          $ref: "#/components/headers/ContentTypeOptions"
        ETag:
          $ref: "#/components/headers/ETag"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/DocumentContentConflictResponse"
    QuotaExceeded:
      description: |
        The reservation would exceed the pooled storage entitlement or the
        per-object limit. The body reports the applicable limits and current
        usage without leaking any other Workspace's data. Quota exhaustion
        never deletes existing content.
      headers:
        Cache-Control:
          $ref: "#/components/headers/CacheControlNoStore"
        X-Request-ID:
          $ref: "#/components/headers/RequestId"
        X-Content-Type-Options:
          $ref: "#/components/headers/ContentTypeOptions"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/QuotaExceededResponse"
    AttachmentRejected:
      description: |
        The uploaded object failed completion validation: a size or checksum
        mismatch, or a detected content type on the execution-risk denylist.
        The object is deleted and its reservation released.
      headers:
        Cache-Control:
          $ref: "#/components/headers/CacheControlNoStore"
        X-Request-ID:
          $ref: "#/components/headers/RequestId"
        X-Content-Type-Options:
          $ref: "#/components/headers/ContentTypeOptions"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    AuthenticationRedirect:
      description: |
        Redirects to the web application. On success, both browser-session
        cookies are set and the default redirect target is /app. That launch
        entry resolves the exact active Workspace to its readable Projects path.
        Account-only sessions continue to Workspace creation; validated explicit
        continuations retain their destination. On an authentication failure, the redirect target is the
        login page with an opaque `error` query parameter and no cookies are
        set; the content-free authentication boundary applies even to this
        browser navigation.
      headers:
        Location:
          description: The web application URL to redirect to.
          schema:
            type: string
            format: uri
        Cache-Control:
          $ref: "#/components/headers/CacheControlNoStore"
    BadRequest:
      description: The request is malformed or fails validation.
      headers:
        Cache-Control:
          $ref: "#/components/headers/CacheControlNoStore"
        X-Request-ID:
          $ref: "#/components/headers/RequestId"
        X-Content-Type-Options:
          $ref: "#/components/headers/ContentTypeOptions"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    Unauthorized:
      description: Authentication is missing, invalid, expired, or revoked.
      headers:
        Cache-Control:
          $ref: "#/components/headers/CacheControlNoStore"
        X-Request-ID:
          $ref: "#/components/headers/RequestId"
        X-Content-Type-Options:
          $ref: "#/components/headers/ContentTypeOptions"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    Forbidden:
      description: The request is forbidden or its CSRF token is missing or invalid.
      headers:
        Cache-Control:
          $ref: "#/components/headers/CacheControlNoStore"
        X-Request-ID:
          $ref: "#/components/headers/RequestId"
        X-Content-Type-Options:
          $ref: "#/components/headers/ContentTypeOptions"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    ReauthenticationRequired:
      description: The action requires a recent authentication challenge.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    UnprocessableEntity:
      description: The request is syntactically valid but violates a domain invariant.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    Gone:
      description: The resource has expired or is no longer available.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    InternalServerError:
      description: The server could not complete the request.
      headers:
        Cache-Control:
          $ref: "#/components/headers/CacheControlNoStore"
        X-Request-ID:
          $ref: "#/components/headers/RequestId"
        X-Content-Type-Options:
          $ref: "#/components/headers/ContentTypeOptions"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    NotFound:
      description: The resource does not exist or is not visible to this principal.
      headers:
        Cache-Control:
          $ref: "#/components/headers/CacheControlNoStore"
        X-Request-ID:
          $ref: "#/components/headers/RequestId"
        X-Content-Type-Options:
          $ref: "#/components/headers/ContentTypeOptions"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    Conflict:
      description: The mutation conflicts with current resource state or idempotency history.
      headers:
        Cache-Control:
          $ref: "#/components/headers/CacheControlNoStore"
        X-Request-ID:
          $ref: "#/components/headers/RequestId"
        X-Content-Type-Options:
          $ref: "#/components/headers/ContentTypeOptions"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    TooManyRequests:
      description: The caller has exceeded an applicable abuse-control limit.
      headers:
        Cache-Control:
          $ref: "#/components/headers/CacheControlNoStore"
        X-Request-ID:
          $ref: "#/components/headers/RequestId"
        X-Content-Type-Options:
          $ref: "#/components/headers/ContentTypeOptions"
        Retry-After:
          description: Seconds until the client may retry.
          schema:
            type: integer
            minimum: 1
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
x-discuss-context-parts:
  response_outcome:
    description: Core-owned partial outcome on a saved response. It preserves recorded results when later work is unfinished. Unknown parts and schema versions remain compatible with older clients.
    schema: assign.discuss.response_outcome.v1
    execution_state: partial; the response did not complete all requested work.
    usage_state: not_started, settled or reconciling; reconciling is not settled usage.
    completed_action_ids: Up to 50 distinct operation UUIDs, each verified against a canonical receipt already projected on this response. An omitted or null group reports no action identities; it is not evidence that every requested action completed.
    recovery: Review recorded receipts before requesting remaining work. Committed changes remain in effect. Existing unknown-outcome guidance and reconciling usage retain canonical status checks; restoration never submits or replays work.
  response_failure:
    description: Safe failure guidance on a terminal failed response. Core excludes raw provider diagnostics. Unknown parts and schema versions remain compatible with older clients.
    schema: assign.discuss.response_failure.v1
    code: "Reviewed category: response_unavailable, response_outcome_unknown, provider_outcome_unknown, tool_outcome_unknown, model_unavailable, tool_contract_changed, decision_target_changed, decision_status_changed, decision_read_unavailable, decision_not_committed, operation_budget_exceeded, insufficient_credits or context_limit_exceeded."
    recovery: Unknown outcomes and unchecked decisions require a canonical message/status and receipt read before another change. Draft restoration requires explicit Send and never replays an approval. Reads create no work or provider call.
    omitted_tool_activity: Optional value 1 when the 20-part envelope replaces one earlier tool activity; immutable receipts and exact decisions remain. A full semantic envelope retains its records and uses conservative generic status recovery.
  web_search:
    description: OpenAI hosted public-web actions recorded from saved provider items; separate from Workspace retrieval.
    provider: openai
    activities: "Up to 20 objects: id, state (in_progress/completed/incomplete/failed), action (search/open_page/find_in_page/other), queries (up to 8 strings), url (nullable safe HTTP(S)). Only completed page visits are linked as visited sources; model prose is not a source record."
  memory_chips:
    description: Explicit private Discuss notes from /remember text, /memories, and /forget UUID through existing send/history operations. Maximum 32 notes of 1000 characters; lists carry 128-character previews. Forget clears saved context, not original history.
    scope: private_discuss
    action: saved | recalled | forgotten
    memories: Objects with id, source_message_id, source_revision, revision and text. Recalled means supplied saved context, not proof of reasoning. Up to 16 notes/8000 UTF-8 bytes supplied to private turns; shared artifact turns exclude them.
