openapi: 3.0.3
info:
  title: Todoha — публичный API
  version: 1.2.0
  description: >
    Публичный API Todoha. Каждый запрос аутентифицируется Personal Access Token
    (PAT)

    через заголовок `Authorization: Bearer todoha_pat_<токен>`.


    Создать токен можно в **Настройки → Токены API**. Полный токен показывается
    **один раз**

    при создании — сохрани его сразу, повторно увидеть нельзя.


    ## Ограничения и советы для email-интеграций


    ⚠️ **Вложения и inline-картинки в v1 НЕ поддерживаются.**


    - Файлы (PDF, DOCX, картинки как MIME-вложения) — загружать нельзя; эндпоинт
    принимает
      только JSON.
    - Inline-картинки в HTML-теле (`<img src="cid:...">`, `<img
    src="data:...">`) — тихо
      вырезаются санитайзером.
    - Тег `<img>` отсутствует в whitelist'е и удаляется целиком (вместе с битыми
    ссылками
      на CID-части писем).

    **Рекомендация для email-интеграций:** добавляй метаданные вложений как
    обычный текст

    в описание — тогда пользователь сможет найти файлы в исходном письме:


    ```

    description: <тело письма>


    📎 Вложения:

    - report.pdf (1.2 MB)

    - photo.jpg (450 KB)

    ```


    Полная поддержка вложений запланирована на v2 (после реализации фичи
    attachments).


    ## Лимиты запросов


    Лимит на каждый PAT (по умолчанию 60 запросов/минуту). При превышении —

    `429 RATE_LIMIT_EXCEEDED` с заголовком `Retry-After` (секунды до следующей
    попытки).
servers:
  - url: /api/public/v1
components:
  securitySchemes:
    PatAuth:
      type: http
      scheme: bearer
      bearerFormat: todoha_pat_*
      description: Personal Access Token. Создать в «Настройки → Токены API».
  schemas:
    Error:
      type: object
      required:
        - code
        - message
      properties:
        code:
          type: string
          description: Машиночитаемый код ошибки.
          example: VALIDATION_ERROR
        message:
          type: string
          description: Человекочитаемое описание ошибки.
          example: title is required
    CreateTaskRequest:
      type: object
      required:
        - title
      properties:
        title:
          type: string
          maxLength: 1000
          description: Заголовок задачи. Обязательное, непустое.
          example: "Из почты: подготовить отчёт"
        description:
          type: string
          maxLength: 100000
          description: >
            Описание задачи. **Авто-распознавание формата:**

            - Если в тексте есть теги из whitelist'а (`<p>`, `<br>`, `<a>`,
            `<ul>`, `<li>`,
              `<h1-3>`, `<strong>`, `<em>` и т.п.) — обрабатывается как HTML и проходит
              санитайзер.
            - Иначе считается обычным текстом: HTML-сущности экранируются, а
            `\n` превращается в `<br>`.


            **НЕ поддерживаются (тихо удаляются санитайзером):** `<img>`,
            `<iframe>`, `<script>`,

            `<style>`, `javascript:`/`data:` ссылки, inline-обработчики
            (`onclick` и т.п.).

            Картинки писем по CID — вырезаются.


            **Файлы-вложения через этот эндпоинт отправить нельзя** — см. раздел
            «Ограничения».
          example: |-
            Привет!

            Встреча в 15:00
        projectId:
          type: string
          nullable: true
          minLength: 1
          maxLength: 10
          description: >
            Идентификатор проекта. **Опционально.** Допустимый формат:

            буквы (**любого алфавита** — латиница, кириллица и т.п.), цифры,

            `-`, `_`. Длина 1–10 символов. Без пробелов и слэшей.


            **Регистр не важен** — значение нормализуется в lowercase. `Work`

            и `work` — один и тот же проект; то же для `Работа` / `работа`.


            Поведение:

            - **Не передан / `null`** — задача создаётся без проекта (Inbox);
              её `code` становится голым числом, равным task id.
            - **Передан, существует у пользователя** (по числовому id или по
            `prefix`) —
              задача попадает в этот проект.
            - **Передан, проекта нет** — проект **создаётся автоматически**:
              `title = prefix = projectId` (в lowercase). Удобно для интеграций
              (email-парсер, n8n), которые знают условные имена групп заранее.
            - **Передан, проект помечен `disabled`** — `409 PROJECT_DISABLED`.

            - **Не соответствует формату** — `400 VALIDATION_ERROR`.
          examples:
            - work
            - работа
            - qa_v2
        priority:
          type: string
          enum:
            - low
            - medium
            - high
            - critical
          default: medium
          description: >
            Приоритет задачи. По умолчанию `medium`. Допустимые значения:

            `low` (низкий), `medium` (средний), `high` (высокий), `critical`
            (критичный).
        status:
          type: string
          enum:
            - open
            - progress
            - wait
            - done
          default: open
          description: |
            Начальный статус задачи. **Опционально**, по умолчанию `open`.
            Позволяет создать задачу сразу в `progress`/`wait`/`done`.
        startDate:
          type: string
          format: date-time
          nullable: true
          description: >
            Дата и время начала задачи. **Опционально.** Принимается **только
            полный

            ISO 8601 с указанием времени и таймзоны:**

            `YYYY-MM-DDTHH:mm:ss(.sss)?(Z|±HH:mm)`.


            Примеры: `2026-05-10T15:00:00Z` (UTC), `2026-05-10T18:00:00+03:00`
            (МСК).

            Сокращённые форматы (`2026-05-10` без времени) → `400
            VALIDATION_ERROR`.
          example: 2026-05-10T15:00:00Z
        dueDate:
          type: string
          format: date-time
          nullable: true
          description: >
            Дата и время дедлайна. **Опционально.** Формат и правила валидации —
            те же,

            что у `startDate`.
          example: 2026-05-15T18:00:00Z
        tags:
          type: array
          items:
            type: string
          nullable: true
          description: >
            Теги задачи. **Опционально.** Только массив строк —
            `["работа","срочно"]`.


            Правила: каждый тег **без пробелов и слэшей**, длина ≤ 100 символов.

            Пустые элементы игнорируются, дубликаты схлопываются.

            Любое другое значение (строка, число, объект) → `400
            VALIDATION_ERROR`.
          example:
            - работа
            - срочно
        parentUuid:
          type: string
          format: uuid
          nullable: true
          description: >
            UUID родительской задачи. **Опционально.** Если передан — задача
            создаётся

            как дочерняя (subtask). Родитель должен принадлежать тому же
            пользователю

            (иначе `404 PARENT_NOT_FOUND`). Максимальная глубина вложенности — 3
            уровня

            (`409 PARENT_CYCLE` при попытке создать более глубокую цепочку).


            Если у родительской задачи указан проект, и текущая создаётся без
            `projectId` —

            проект **наследуется** от родителя.
          example: 01234567-89ab-cdef-0123-456789abcdef
        todos:
          type: array
          nullable: true
          items:
            oneOf:
              - type: string
                description: "Back-compat: строка → todo со статусом completed:false."
              - type: object
                required:
                  - text
                properties:
                  id:
                    type: string
                    description: UUID существующего todo (опционально). При совпадении — uuid и
                      completedAt сохраняются (если completed не изменился).
                  text:
                    type: string
                    maxLength: 2000
                    description: Текст todo (обязательно, непустая строка).
                  completed:
                    type: boolean
                    description: Статус выполнения (по умолчанию false).
          description: >
            Список todo-пунктов. Каждый элемент — строка (back-compat) или
            объект `{id?, text, completed?}`.


            **При `POST /tasks`** (`create_task`): `id` игнорируется — сервер
            генерирует свежие uuid.

            Каждая строка / объект → todo с указанным `completed` (по умолчанию
            false) и `position` = индекс.


            **При `PATCH /tasks/{uuid}`** (`update_task`):

            - **не передано** — todos не трогаются;

            - **`[]`** — все существующие todos удаляются;

            - **`[...]`** — replace с id-preserve: items с существующим `id`
            сохраняют свой uuid и `completedAt`
              (если `completed` не изменился); items без `id` вставляются как новые; items не вошедшие в список удаляются.

            Лимиты: до 50 элементов, каждый ≤ 2000 символов. Пустые строки
            запрещены.
          example:
            - Подготовить slides
            - text: Согласовать с командой
              completed: false
            - id: 3fa85f64-5717-4562-b3fc-2c963f66afa6
              text: Отправить ссылку
              completed: true
    UpdateTaskRequest:
      type: object
      description: >
        Partial update задачи. Передавай только те поля, которые надо изменить.

        Все поля опциональны; правила валидации — те же, что у
        `CreateTaskRequest`,

        плюс `status` и особая семантика `todos` (см. описание поля в
        CreateTaskRequest).
      properties:
        title:
          type: string
          maxLength: 1000
        description:
          type: string
          maxLength: 100000
        projectId:
          type: string
          nullable: true
          minLength: 1
          maxLength: 10
        priority:
          type: string
          enum:
            - low
            - medium
            - high
            - critical
        status:
          type: string
          enum:
            - open
            - progress
            - wait
            - done
          description: >
            Статус задачи. При переходе в `done` — `closedAt` ставится
            автоматически

            (текущее время сервера).
        startDate:
          type: string
          format: date-time
          nullable: true
        dueDate:
          type: string
          format: date-time
          nullable: true
        tags:
          type: array
          items:
            type: string
          nullable: true
        parentUuid:
          type: string
          format: uuid
          nullable: true
          description: |
            UUID нового родителя или `null` (открепить задачу от родителя).
            Само-привязка (`parentUuid == uuid`) → `409 PARENT_CYCLE`.
        todos:
          type: array
          nullable: true
          items:
            oneOf:
              - type: string
              - type: object
                required:
                  - text
                properties:
                  id:
                    type: string
                    description: UUID существующего todo (id-preserve).
                  text:
                    type: string
                    maxLength: 2000
                  completed:
                    type: boolean
          description: >
            Заменить список todos. Семантика: не передано = не трогать; `[]` =
            очистить; `[...]` = replace с id-preserve.

            See `CreateTaskRequest.todos` для полного описания формата.
        todoUpdates:
          type: array
          nullable: true
          items:
            type: object
            required:
              - id
              - completed
            properties:
              id:
                type: string
                description: UUID todo для точечного toggle.
              completed:
                type: boolean
                description: Новый статус выполнения.
          description: >
            Точечный toggle completed для конкретных todos по id — не заменяет
            список.

            Если оба поля `todos` и `todoUpdates` переданы: сначала применяется
            `todos` (replace), затем `todoUpdates` (в новом списке).

            Несуществующие `id` игнорируются (idempotent).
    Task:
      type: object
      description: >
        Полная модель задачи, возвращаемая `GET /tasks/{uuid}`, `PATCH
        /tasks/{uuid}`,

        `POST /tasks/{uuid}/complete`, и каждая запись в `GET /tasks`.
      required:
        - uuid
        - title
        - status
        - priority
        - createdAt
      properties:
        uuid:
          type: string
          format: uuid
        id:
          type: string
          description: Последовательный ID для UI (per-user).
        code:
          type: string
          nullable: true
          description: Человеко-читаемый код, e.g. `work-12` или `#-3`.
        title:
          type: string
        description:
          type: string
        projectGuid:
          type: string
          format: uuid
          nullable: true
        priority:
          type: string
          enum:
            - low
            - medium
            - high
            - critical
        status:
          type: string
          enum:
            - open
            - progress
            - wait
            - done
        startDate:
          type: string
          format: date-time
          nullable: true
        dueDate:
          type: string
          format: date-time
          nullable: true
        closedAt:
          type: string
          format: date-time
          nullable: true
        createdAt:
          type: string
          format: date-time
        changedAt:
          type: string
          format: date-time
          nullable: true
        parentGuid:
          type: string
          format: uuid
          nullable: true
        tags:
          type: array
          items:
            type: string
        todos:
          type: array
          items:
            type: object
            required:
              - text
              - completed
            properties:
              id:
                type: string
              text:
                type: string
              completed:
                type: boolean
              completedAt:
                type: string
                format: date-time
                nullable: true
        comments:
          type: array
          items:
            type: object
            required:
              - id
              - text
            properties:
              id:
                type: string
              text:
                type: string
              createdAt:
                type: string
                format: date-time
              updatedAt:
                type: string
                format: date-time
                nullable: true
              source:
                type: string
                nullable: true
                description: AI-атрибуция автора коммента — `human` или имя PAT; `null` для
                  legacy.
        createdBy:
          type: string
          nullable: true
          description: AI-атрибуция — кто создал задачу (`human` или имя PAT). `null` для
            legacy/неизвестно. Immutable.
        lastEditedBy:
          type: string
          nullable: true
          description: AI-атрибуция — кто правил последним. Ракурс «AI» = значение не
            равно `human` (и не null).
        goalUuid:
          type: string
          format: uuid
          nullable: true
          description: UUID цели, к которой привязана задача (task→goal). null если не
            привязана.
        kv:
          type: array
          nullable: true
          description: >
            Пары «ключ-значение» задачи («Свойства»). Отсутствует если пар нет.

            Map-семантика: ключ уникален в рамках задачи; повторный set
            перезаписывает значение.

            Изменяются через `PUT /tasks/{uuid}/kv/{key}`, `DELETE
            /tasks/{uuid}/kv/{key}`,

            или поле `kv` в `PATCH /tasks/{uuid}` / MCP-tools `set_task_kv` /
            `delete_task_kv`.
          items:
            type: object
            required:
              - key
              - value
            properties:
              key:
                type: string
                maxLength: 100
                description: Ключ свойства (непустая строка, ≤100 символов).
              value:
                type: string
                maxLength: 1000
                description: Значение свойства (строка, ≤1000 символов).
    TaskListResponse:
      type: object
      required:
        - items
        - total
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/Task"
        total:
          type: integer
    Goal:
      type: object
      description: >
        Цель пользователя. Доступна read-only через публичный API
        `/api/public/v1/goals`

        и MCP-tools `list_goals` / `get_goal`. Задачи связываются с целью через
        поле

        `goalUuid` в Task.
      required:
        - uuid
        - id
        - title
        - description
        - status
        - sortOrder
        - createdAt
      properties:
        uuid:
          type: string
          format: uuid
          description: Глобальный идентификатор цели.
        id:
          type: string
          description: Последовательный ID цели (для UI).
        title:
          type: string
          maxLength: 200
          description: Название цели.
        description:
          type: string
          description: Описание цели.
        deadline:
          type: string
          format: date-time
          nullable: true
          description: Срок достижения цели (ISO 8601) или null.
        status:
          type: string
          enum:
            - active
            - done
          description: Статус цели.
        sortOrder:
          type: integer
          description: Порядок отображения цели (меньше — выше).
        createdAt:
          type: string
          format: date-time
          description: Время создания (ISO 8601, UTC).
        changedAt:
          type: string
          format: date-time
          nullable: true
          description: Время последнего изменения (ISO 8601, UTC) или null.
    GoalListResponse:
      type: object
      required:
        - items
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/Goal"
    Contact:
      type: object
      description: Контакт из модуля Жизнь.
      required:
        - uuid
        - firstName
        - createdAt
        - changedAt
      properties:
        uuid:
          type: string
          format: uuid
        firstName:
          type: string
        lastName:
          type: string
        middleName:
          type: string
          nullable: true
        shortName:
          type: string
          nullable: true
          description: Short name / alias / nickname for quick search
        organization:
          type: string
          nullable: true
        birthday:
          type: string
          nullable: true
          description: YYYY-MM-DD или null
        note:
          type: string
          nullable: true
        phone:
          type: string
          nullable: true
        email:
          type: string
          nullable: true
        telegram:
          type: string
          nullable: true
        tags:
          type: array
          items:
            type: string
        createdAt:
          type: string
          format: date-time
        changedAt:
          type: string
          format: date-time
    Note:
      type: object
      description: Заметка из модуля Жизнь.
      required:
        - uuid
        - content
        - createdAt
        - changedAt
      properties:
        uuid:
          type: string
          format: uuid
        title:
          type: string
          nullable: true
        content:
          type: string
        contactUuid:
          type: string
          format: uuid
          nullable: true
        tags:
          type: array
          items:
            type: string
        createdAt:
          type: string
          format: date-time
        changedAt:
          type: string
          format: date-time
    Interaction:
      type: object
      description: Взаимодействие (звонок/встреча/сообщение) из модуля Жизнь.
      required:
        - uuid
        - kind
        - happenedAt
        - summary
        - createdAt
        - changedAt
      properties:
        uuid:
          type: string
          format: uuid
        kind:
          type: string
          enum:
            - call
            - meeting
            - message
            - other
        happenedAt:
          type: string
          format: date-time
        summary:
          type: string
        contactUuid:
          type: string
          format: uuid
          nullable: true
        tags:
          type: array
          items:
            type: string
        createdAt:
          type: string
          format: date-time
        changedAt:
          type: string
          format: date-time
    JournalEntry:
      type: object
      description: Запись дневника из модуля Жизнь.
      required:
        - uuid
        - entryDate
        - content
        - createdAt
        - changedAt
      properties:
        uuid:
          type: string
          format: uuid
        entryDate:
          type: string
          description: YYYY-MM-DD
        mood:
          type: string
          nullable: true
        content:
          type: string
        tags:
          type: array
          items:
            type: string
        createdAt:
          type: string
          format: date-time
        changedAt:
          type: string
          format: date-time
    InboxItem:
      type: object
      description: Элемент быстрой фиксации (inbox) из модуля Жизнь.
      required:
        - uuid
        - content
        - createdAt
        - changedAt
      properties:
        uuid:
          type: string
          format: uuid
        content:
          type: string
        createdAt:
          type: string
          format: date-time
        changedAt:
          type: string
          format: date-time
    Relation:
      type: object
      description: >
        Направленная связь между двумя объектами (задача, контакт, заметка,
        взаимодействие,

        дневник, inbox). Создаётся через `create_relation`, перечисляется через
        `list_relations`.
      required:
        - uuid
        - fromType
        - fromId
        - toType
        - toId
        - type
        - createdAt
      properties:
        uuid:
          type: string
          format: uuid
        fromType:
          type: string
          enum:
            - task
            - contact
            - note
            - interaction
            - journal
            - inbox
        fromId:
          type: string
          format: uuid
        toType:
          type: string
          enum:
            - task
            - contact
            - note
            - interaction
            - journal
            - inbox
        toId:
          type: string
          format: uuid
        type:
          type: string
          enum:
            - depends_on
            - related_to
            - duplicates
            - follow_up
            - about
            - mentions
        createdAt:
          type: string
          format: date-time
        changedAt:
          type: string
          format: date-time
          nullable: true
    Action:
      type: object
      description: >
        Определение кнопки (action) из модуля Действия (aclick). Читается через

        MCP-tool `act_list_actions` / `act_get_action`, создаётся через
        `act_create_action`.

        Тип кнопки задаётся при создании и **не изменяется**. Конфигурация
        (`config`)

        зависит от типа — см. §config в api.md.
      required:
        - id
        - name
        - type
        - config
        - sort_order
        - completed_at
        - keep_active
        - color
        - created_at
      properties:
        id:
          type: integer
          description: Числовой id (per-user)
        name:
          type: string
          maxLength: 200
        type:
          type: string
          enum:
            - BtnStartEnd
            - BtnClickCount
            - BtnMultiClick
            - BtnRemind
        config:
          type: object
          description: >
            Конфигурация по типу:

            BtnStartEnd: { labelStart?, labelEnd? }

            BtnClickCount: { label?, targetCount? (≥1), colorVariant?
            ("gray"|"blue"|"warm"|"lavender"|"sage"), showTimestamps? }

            BtnMultiClick: { buttons: [{label,color?}][], totalClicksLimit:
            number|null, labelStart?, labelEnd?, withUndo? }

            BtnRemind: { label?, minutesOptions?: number[] }
        sort_order:
          type: integer
        completed_at:
          type: string
          format: date-time
          nullable: true
        keep_active:
          type: boolean
        color:
          type: string
          nullable: true
          description: "#rrggbb | null"
        created_at:
          type: string
          format: date-time
    ActivityEvent:
      type: object
      description: >
        Запись лога активности (aclick). Создаётся через MCP-tool
        `act_log_event`

        или UI Runner'ом при нажатии кнопки.

        `action_name` денормализован на момент события (сохраняется даже после
        удаления кнопки).
      required:
        - id
        - action_id
        - action_name
        - kind
        - payload
        - occurred_at
      properties:
        id:
          type: integer
        action_id:
          type: integer
        action_name:
          type: string
          description: Денормализованное имя кнопки
        kind:
          type: string
          enum:
            - click
            - start
            - end
            - skip
            - undo
          description: >
            Вид события. payload-формы по type+kind:

            BtnStartEnd start→null; end→{durationSec:number}

            BtnClickCount click|undo→null

            BtnMultiClick start→null; click→{buttonIndex,buttonLabel};
            undo→{buttonIndex}; end→{totalClicks}

            BtnRemind click→{minutes:number}
        payload:
          type: object
          nullable: true
        occurred_at:
          type: string
          format: date-time
    CreateTaskResponse:
      type: object
      required:
        - uuid
      properties:
        uuid:
          type: string
          format: uuid
          description: Глобальный уникальный идентификатор созданной задачи. Используй его
            для последующих API-вызовов и синхронизации.
          example: 01234567-89ab-cdef-0123-456789abcdef
        warnings:
          type: array
          description: >
            Поле присутствует только если санитайзер удалил какие-то теги из
            описания.

            Каждая запись описывает один класс удалённых тегов (например

            `1 unsupported tag stripped: img`). Используй это поле для отладки
            интеграции,

            если содержимое исчезает.
          items:
            type: string
          example:
            - "1 unsupported tag stripped: img"
        idempotent:
          type: boolean
          description: >
            `true` — ответ восстановлен из кеша по `Idempotency-Key` (повторный
            POST с тем же ключом).

            `undefined` (отсутствует) — задача создана заново.
paths:
  /:
    get:
      summary: API descriptor
      description: >
        Корневой descriptor публичного API. Возвращает метаданные: имя, версию,
        как

        получить PAT, URL'ы всех endpoint'ов (REST, OpenAPI, Swagger UI, MCP,
        info page),

        список MCP-tools.


        **Не требует аутентификации** — удобен для discovery (LLM-агентам,
        скриптам,

        мониторингу).
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                example:
                  name: Todoha — публичный API
                  version: 1.2.0
                  endpoints:
                    tasks: /api/public/v1/tasks
                    goals: /api/public/v1/goals
                    mcp: /api/public/v1/mcp
                    docs: /api/public/v1/docs
                    openapi: /api/public/v1/openapi.yaml
                    mcpInfo: /api/public/v1/mcp-info
                  mcp:
                    url: /api/public/v1/mcp
                    transport: streamable-http
                    tools:
                      - create_task
                      - list_tasks
                      - get_task
                      - update_task
                      - complete_task
                      - delete_task
                      - add_comment
                      - list_goals
                      - get_goal
  /mcp-info:
    get:
      summary: HTML-страница с инструкциями по подключению MCP
      description: |
        Самодостаточная HTML-страница: что такое MCP, как создать PAT, как
        подключить Todoha к Claude Desktop / Claude Code / Cursor, примеры curl,
        безопасность, мониторинг. Без аутентификации.
      responses:
        "200":
          description: HTML
          content:
            text/html:
              schema:
                type: string
  /tasks:
    post:
      summary: Создать задачу
      description: >
        Создаёт новую задачу для пользователя, которому принадлежит PAT.

        Возвращает глобальный `uuid` задачи. Если в описании были теги,
        удалённые

        санитайзером, ответ будет содержать поле `warnings`.


        **Idempotency.** При сетевых retry передавай header `Idempotency-Key:
        <стабильный-ключ>`

        (любая строка ≤255 символов). Повторный POST с тем же ключом для того же
        PAT

        вернёт **тот же** `uuid` и поле `idempotent: true` — дубль задачи в БД
        не появится.

        Ключ привязан к user_uuid токена, между пользователями не пересекается.
      security:
        - PatAuth: []
      parameters:
        - in: header
          name: Idempotency-Key
          required: false
          schema:
            type: string
            maxLength: 255
          description: Клиентский стабильный ключ для retry-safe создания.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateTaskRequest"
            examples:
              plain:
                summary: Обычный текст из email
                value:
                  title: "Из почты: запрос отчёта"
                  description: |-
                    Здравствуйте,

                    Пришлите отчёт за май до пятницы.

                    Спасибо!
                  priority: high
              html:
                summary: HTML-тело письма
                value:
                  title: "Из почты: подтверждение"
                  description: <p>Здравствуйте!</p><p>Подтверждаю встречу.</p>
                  projectId: a
              with-dates:
                summary: С датами начала и дедлайна (полный ISO 8601)
                value:
                  title: Подготовить отчёт
                  startDate: 2026-05-10T09:00:00Z
                  dueDate: 2026-05-15T18:00:00Z
                  priority: high
              with-tags:
                summary: С тегами (массив строк)
                value:
                  title: Подготовить отчёт
                  tags:
                    - работа
                    - срочно
              with-stripped-image:
                summary: HTML с inline-картинкой (картинка будет удалена)
                value:
                  title: Скриншот
                  description: <p>см. скрин</p><img src="cid:img1">
              subtask:
                summary: Дочерняя задача (parentUuid)
                value:
                  title: Подзадача 1
                  parentUuid: 01234567-89ab-cdef-0123-456789abcdef
              with-todos:
                summary: С чек-листом (todos как массив строк)
                value:
                  title: Подготовиться к встрече
                  todos:
                    - slides
                    - повестка
                    - пригласить участников
      responses:
        "201":
          description: Задача создана
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CreateTaskResponse"
        "400":
          description: Ошибка валидации (отсутствует title, неверный priority/todos и т.п.)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: PAT отсутствует или недействителен
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: >
            Указанный `projectId` или `parentUuid` не найден у этого
            пользователя

            (коды: `PARENT_NOT_FOUND`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "409":
          description: |
            Конфликт: `PROJECT_DISABLED` (проект отключён) или
            `PARENT_CYCLE` (превышена максимальная глубина subtask-цепочки).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "429":
          description: Превышен лимит запросов
          headers:
            Retry-After:
              description: Сколько секунд подождать до следующей попытки.
              schema:
                type: integer
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
    get:
      summary: Список задач
      description: >
        Возвращает задачи текущего пользователя. Поддерживает фильтрацию по
        `status`,

        `projectId`, `parentUuid` и пагинацию (`limit` 1–200, default 50;
        `offset` ≥0).
      security:
        - PatAuth: []
      parameters:
        - in: query
          name: status
          schema:
            type: string
            enum:
              - open
              - progress
              - wait
              - done
        - in: query
          name: projectId
          schema:
            type: string
            minLength: 1
            maxLength: 10
          description: id или prefix существующего проекта.
        - in: query
          name: parentUuid
          schema:
            type: string
          description: UUID родителя для фильтра подзадач, либо строка `null` — только
            корневые.
        - in: query
          name: editedByAi
          schema:
            type: boolean
          description: >
            Ракурс «AI». `true` — только задачи, которые последним правил агент

            (`last_edited_by` != `human`, не null); `false` — правленные
            человеком/неизвестно.

            Работает сквозь все проекты.
        - in: query
          name: createdBy
          schema:
            type: string
          description: Точное совпадение по автору создания (`human` или имя PAT, напр.
            `claude-code`).
        - in: query
          name: lastEditedBy
          schema:
            type: string
          description: Точное совпадение по автору последней правки.
        - in: query
          name: limit
          schema:
            type: integer
            minimum: 1
            maximum: 200
            default: 50
        - in: query
          name: offset
          schema:
            type: integer
            minimum: 0
            default: 0
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TaskListResponse"
        "400":
          description: Невалидный фильтр
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: PAT отсутствует или недействителен
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /tasks/{uuid}:
    parameters:
      - in: path
        name: uuid
        required: true
        schema:
          type: string
          format: uuid
    get:
      summary: Получить задачу
      security:
        - PatAuth: []
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Task"
        "400":
          description: Невалидный UUID
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Задача не найдена (`TASK_NOT_FOUND`)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
    patch:
      summary: Обновить задачу (partial)
      security:
        - PatAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateTaskRequest"
            examples:
              rename:
                summary: Переименовать
                value:
                  title: Новый заголовок
              replace-todos:
                summary: Заменить todos (старые удаляются)
                value:
                  todos:
                    - новый пункт 1
                    - новый пункт 2
              clear-todos:
                summary: Очистить todos
                value:
                  todos: []
              mark-done:
                summary: Отметить как сделанную (закроется closedAt сервером)
                value:
                  status: done
              re-parent:
                summary: Перепривязать к другому родителю
                value:
                  parentUuid: 01234567-89ab-cdef-0123-456789abcdef
              detach:
                summary: Открепить от родителя
                value:
                  parentUuid: null
      responses:
        "200":
          description: Обновлено
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Task"
        "400":
          description: Ошибка валидации
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Задача или `parentUuid` не найдены
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "409":
          description: "`PARENT_CYCLE` или `PROJECT_DISABLED`"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
    delete:
      summary: Удалить задачу
      description: |
        Удаляет задачу и каскадно — все её подзадачи (FK ON DELETE CASCADE).
      security:
        - PatAuth: []
      responses:
        "204":
          description: Удалено
        "404":
          description: Задача не найдена
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /tasks/{uuid}/complete:
    parameters:
      - in: path
        name: uuid
        required: true
        schema:
          type: string
          format: uuid
    post:
      summary: Отметить задачу как выполненную
      description: >
        Идемпотентный шорткат: ставит `status=done` и `closedAt=now()`.
        Повторный вызов

        на уже завершённой задаче возвращает 200 без изменений.
      security:
        - PatAuth: []
      responses:
        "200":
          description: Задача в статусе `done`
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Task"
        "404":
          description: Задача не найдена
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /tasks/{uuid}/comments:
    parameters:
      - in: path
        name: uuid
        required: true
        schema:
          type: string
          format: uuid
    post:
      summary: Добавить комментарий к задаче
      description: >
        Добавляет комментарий. Поле `source` проставляется автоматически из PAT

        (имя токена, напр. `claude-code`) — человек потом видит, какой агент
        написал.

        Текст санитайзится как описание (HTML whitelist), лимит —
        `commentTextMax`.
      security:
        - PatAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - text
              properties:
                text:
                  type: string
                  description: Текст комментария (plain или HTML; санитайзится). Непустой.
            examples:
              plain:
                summary: Простой комментарий
                value:
                  text: Готово, отчёт во вложении.
      responses:
        "201":
          description: Комментарий создан
          content:
            application/json:
              schema:
                type: object
                required:
                  - id
                  - text
                  - createdAt
                properties:
                  id:
                    type: string
                  text:
                    type: string
                  createdAt:
                    type: string
                    format: date-time
                  source:
                    type: string
                    nullable: true
                    description: AI-атрибуция автора (`human` или имя PAT).
        "400":
          description: Пустой текст или превышен лимит длины
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Задача не найдена (`TASK_NOT_FOUND`)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /tasks/{uuid}/kv:
    parameters:
      - in: path
        name: uuid
        required: true
        schema:
          type: string
          format: uuid
        description: UUID задачи.
    get:
      summary: Прочитать все свойства задачи (kv)
      description: >
        Лёгкая альтернатива `GET /tasks/{uuid}` когда нужны только свойства.

        Возвращает полный список пар ключ-значение. `kv` также присутствует в
        самой задаче.
      security:
        - PatAuth: []
      responses:
        "200":
          description: OK — список kv
          content:
            application/json:
              schema:
                type: object
                properties:
                  kv:
                    type: array
                    items:
                      type: object
                      required:
                        - key
                        - value
                      properties:
                        key:
                          type: string
                        value:
                          type: string
        "404":
          description: Задача не найдена (`TASK_NOT_FOUND`)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /tasks/{uuid}/kv/{key}:
    parameters:
      - in: path
        name: uuid
        required: true
        schema:
          type: string
          format: uuid
        description: UUID задачи.
      - in: path
        name: key
        required: true
        schema:
          type: string
          minLength: 1
          maxLength: 100
        description: Ключ свойства (≤100 символов).
    put:
      summary: Установить свойство задачи (upsert kv)
      description: >
        Map-семантика: если ключ уже существует — перезаписывает значение; иначе
        вставляет новую пару.

        Лимит: максимум 50 пар на задачу (по умолчанию). Возвращает полный
        список пар задачи.
      security:
        - PatAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - value
              properties:
                value:
                  type: string
                  maxLength: 1000
                  description: Значение свойства (строка, ≤1000 символов).
            examples:
              set:
                summary: Поставить значение
                value:
                  value: passing
      responses:
        "200":
          description: OK — возвращает обновлённый список kv
          content:
            application/json:
              schema:
                type: object
                properties:
                  kv:
                    type: array
                    items:
                      type: object
                      required:
                        - key
                        - value
                      properties:
                        key:
                          type: string
                        value:
                          type: string
        "400":
          description: Ошибка валидации (key/value слишком длинные, превышен лимит пар)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Задача не найдена (`TASK_NOT_FOUND`)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
    delete:
      summary: Удалить свойство задачи (kv)
      description: >
        Идемпотентный: успех даже если ключ отсутствует. 404 только если задача
        не найдена.
      security:
        - PatAuth: []
      responses:
        "204":
          description: Удалено (или ключ уже отсутствовал)
        "404":
          description: Задача не найдена (`TASK_NOT_FOUND`)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /goals:
    get:
      summary: Список целей
      description: >
        Возвращает цели текущего пользователя (read-only). Поддерживает
        необязательный

        фильтр по статусу (`active` или `done`). Цели — контекст «зачем» для
        задач:

        задача связана с целью через поле `goalUuid`.
      security:
        - PatAuth: []
      parameters:
        - in: query
          name: status
          schema:
            type: string
            enum:
              - active
              - done
          description: Фильтровать по статусу цели.
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/GoalListResponse"
        "400":
          description: Невалидный фильтр (например, неверное значение `status`)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: PAT отсутствует или недействителен
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /goals/{uuid}:
    parameters:
      - in: path
        name: uuid
        required: true
        schema:
          type: string
          format: uuid
        description: UUID цели.
    get:
      summary: Получить цель
      description: |
        Возвращает одну цель по UUID. Цель другого пользователя возвращает
        `404 GOAL_NOT_FOUND` — без enumeration.
      security:
        - PatAuth: []
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Goal"
        "400":
          description: Невалидный UUID (формат)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: PAT отсутствует или недействителен
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Цель не найдена или принадлежит другому пользователю
            (`GOAL_NOT_FOUND`)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /mcp:
    post:
      summary: MCP endpoint (JSON-RPC 2.0, Streamable HTTP)
      description: >
        Endpoint для **Model Context Protocol**: позволяет LLM-агентам (Claude
        Desktop,

        Claude Code, Cursor и т.п.) работать с задачами и модулем Жизни через
        единый канал.


        **Аутентификация** — тот же Bearer PAT, что и для REST. Каждый JSON-RPC
        запрос

        привязан к пользователю PAT'а.


        **Scopes (PAT):**


        | Scope              | Даёт доступ |

        |--------------------|-------------|

        | *(нет scopes)*     | tasks:read + tasks:write — обратная совместимость
        со старыми PAT |

        | `tasks:read`       | list_tasks, get_task, list_goals, get_goal,
        get_task_kv |

        | `tasks:write`      | create_task, update_task, complete_task,
        delete_task, add_comment, set_task_kv, delete_task_kv |

        | `life:read`        | life_list_*, life_get_*, life_search,
        life_search_contacts |

        | `life:write`       | life_create_*, life_update_*, life_delete_*,
        life_sort_inbox |

        | `relations:read`   | *(опционально)* list_relations — иначе выводится
        из прав на объект-конец |

        | `relations:write`  | *(опционально)* create_relation, delete_relation
        — иначе выводится из прав на объекты-концы |

        | `actions:read`     | act_list_actions, act_get_action, act_list_events
        |

        | `actions:write`    | act_create_action, act_update_action,
        act_reorder_actions, act_complete_action, act_uncomplete_action,
        act_delete_action, act_log_event, act_delete_event |


        **Важно:** `actions:*` — только явный grant. Старые PAT без scopes НЕ
        получают `actions:*` автоматически.


        Указывать scopes при создании PAT через `POST /api/v1/pats` → поле
        `scopes`.


        **Task tools (scope: tasks:read / tasks:write; legacy PATs без scope
        получают оба автоматически):**


        | Tool            | Назначение |

        |-----------------|------------|

        | `create_task`   | Создать задачу (поддерживает `parentUuid`, `todos`,
        `status`). |

        | `list_tasks`    | Список задач с фильтрами. |

        | `get_task`      | Получить одну задачу по `uuid`. |

        | `update_task`   | Partial update. |

        | `complete_task` | Шорткат: status=done. |

        | `delete_task`   | Удалить (cascade на подзадачи). |

        | `add_comment`   | Добавить комментарий. |

        | `list_goals`    | Список целей (read-only). |

        | `get_goal`      | Получить одну цель по `uuid`. |

        | `get_task_kv`   | Прочитать свойства задачи (kv). |

        | `set_task_kv`   | Установить свойство задачи. |

        | `delete_task_kv`| Удалить свойство задачи. |


        **Life tools (scope: life:read / life:write):**


        | Tool                      | Scope       | Назначение |

        |---------------------------|-------------|------------|

        | `life_list_contacts`      | life:read   | Список контактов |

        | `life_get_contact`        | life:read   | Контакт по uuid |

        | `life_create_contact`     | life:write  | Создать контакт |

        | `life_update_contact`     | life:write  | Обновить контакт |

        | `life_delete_contact`     | life:write  | Soft-delete контакт |

        | `life_list_notes`         | life:read   | Список заметок |

        | `life_get_note`           | life:read   | Заметка по uuid |

        | `life_create_note`        | life:write  | Создать заметку |

        | `life_update_note`        | life:write  | Обновить заметку |

        | `life_delete_note`        | life:write  | Soft-delete заметка |

        | `life_list_interactions`  | life:read   | Список взаимодействий |

        | `life_get_interaction`    | life:read   | Взаимодействие по uuid |

        | `life_create_interaction` | life:write  | Создать взаимодействие |

        | `life_update_interaction` | life:write  | Обновить взаимодействие |

        | `life_delete_interaction` | life:write  | Soft-delete |

        | `life_list_journal`       | life:read   | Список дневниковых записей |

        | `life_get_journal`        | life:read   | Запись дневника по uuid |

        | `life_create_journal`     | life:write  | Создать запись дневника |

        | `life_update_journal`     | life:write  | Обновить запись |

        | `life_delete_journal`     | life:write  | Soft-delete |

        | `life_list_inbox`         | life:read   | Список inbox-элементов |

        | `life_get_inbox`          | life:read   | Inbox-элемент по uuid |

        | `life_create_inbox`       | life:write  | Создать inbox-элемент |

        | `life_update_inbox`       | life:write  | Обновить inbox-элемент |

        | `life_delete_inbox`       | life:write  | Soft-delete |

        | `life_sort_inbox`         | life:write  | Переместить inbox → модуль
        (транзакционно) |

        | `life_search`             | life:read   | Кросс-модульный поиск |

        | `life_search_contacts`    | life:read   | Поиск контактов |


        **Relation tools (scope выводится из прав на объекты-концы):**


        Связь разрешена, если у PAT есть право на группы **обоих** концов:
        `task` → группа `tasks`,

        Life-объекты (`contact`/`note`/`interaction`/`journal`/`inbox`) → группа
        `life`. Явный

        `relations:read`/`relations:write` тоже авторизует (обратная
        совместимость).


        | Tool              | Требуемый доступ | Назначение |

        |-------------------|------------------|------------|

        | `create_relation` | write на обе группы концов (task↔contact →
        `tasks:write` + `life:write`) | Создать связь между двумя объектами |

        | `list_relations`  | read на группу объекта (task → `tasks:read`, Life
        → `life:read`) | Список связей объекта (outgoing + incoming) |

        | `delete_relation` | write на обе группы концов связи | Удалить связь
        по uuid |


        **Actions tools (scope: actions:read / actions:write — явный grant, не
        авто):**


        | Tool                    | Scope            | Назначение |

        |-------------------------|------------------|------------|

        | `act_list_actions`      | actions:read     | Список определений кнопок
        |

        | `act_get_action`        | actions:read     | Получить кнопку по id |

        | `act_list_events`       | actions:read     | Лог активности (фильтр по
        дате, TZ-aware) |

        | `act_create_action`     | actions:write    | Создать новую кнопку |

        | `act_update_action`     | actions:write    | Обновить кнопку (тип
        нельзя менять) |

        | `act_reorder_actions`   | actions:write    | Изменить порядок кнопок |

        | `act_complete_action`   | actions:write    | Отметить кнопку
        выполненной |

        | `act_uncomplete_action` | actions:write    | Снять отметку выполнения
        |

        | `act_delete_action`     | actions:write    | Удалить кнопку (cascade
        на события) |

        | `act_log_event`         | actions:write    | Записать событие (сервер
        денорм. имя) |

        | `act_delete_event`      | actions:write    | Удалить событие из лога |


        Подключение клиента: в конфиге MCP-клиента указать URL

        `https://<your-host>/api/public/v1/mcp` и header

        `Authorization: Bearer todoha_pat_<token>`.


        Подробности протокола: <https://modelcontextprotocol.io/>.
      security:
        - PatAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              description: JSON-RPC 2.0 запрос MCP-протокола.
              example:
                jsonrpc: "2.0"
                id: 1
                method: tools/list
      responses:
        "200":
          description: JSON-RPC ответ либо event-stream (SSE)
        "401":
          description: PAT отсутствует или недействителен
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
