# commands Specification ## Purpose Контракт команд моста: каждая команда — желаемое состояние одной задачи Jira (`ensure_*`) или чтение состояния (`refresh`). Здесь типы команд и их параметры, временные ключи внутри пакета, порядок выполнения, политика темпа, обработка ошибок, повторная выдача и подтверждение по состоянию зеркала. ## Requirements ### Requirement: Команда — желаемый результат на одной задаче Каждая команда SHALL описывать желаемое состояние одной задачи Jira и выполняться агентом как «прочитать → сравнить → записать только разницу». Повторное выполнение любой команды с тем же результатом в Jira SHALL завершаться `done` без записи. `fetch_attachment` в Jira не пишет: она читает файл вложения и передаёт его хабу; повтор безопасен. Типы команд и их контракт: | Тип | Параметры | Что читает | Что пишет | |---|---|---|---| | `ensure_issue` | `temp_key` (`new:`), `issue_type` (`task`/`epic`/`bug`), `summary`, `description`, `priority`, `duedate`, `planned_start`, `planned_end`, `labels`, `epic_key`, `assignee` | поиск `project = AND reporter = currentUser() AND created >= -7d`, точное сравнение `summary` | при отсутствии — `POST /issue` с типом из `issue_types.` (для эпика — с `epic_name_field_id`); результат `key` | | `ensure_fields` | `key`, любое подмножество: `summary`, `description`, `priority`, `duedate`, `planned_start`, `planned_end`, `labels_add`, `labels_remove`, `epic_key`, `assignee`, `story_points` (число ≥ 0 или `null` — очистить); при `story_points` `board_id` хаб дописывает при постановке (от клиента — 422) | `GET /issue/{key}` — если есть поля, кроме оценки; оценка — `GET /rest/agile/1.0/issue/{key}/estimation?boardId=` | `PUT /issue/{key}` только с отличающимися полями, кроме оценки; эпик — по `epic_link.mode`; оценка отличается — `PUT /rest/agile/1.0/issue/{key}/estimation?boardId=` с `{value}` (поле оценки доски, экран редактирования Jira не нужен); команда без `board_id` — оценка через `PUT /issue/{key}` в поле story points, как в v15 (см. `agent`, «Спринты, ранг и оценки») | | `ensure_comment` | `key`, `marker`, `body` | `GET /issue/{key}/comment` | комментарий с `marker` в начале: `PUT` если текст отличается, `POST` если нет | | `ensure_status` | `key`, `target` (имя статуса или список-путь) | `GET /issue/{key}` (статус), `GET /issue/{key}/transitions` на каждом шаге | `POST /transitions` по шагам пути; при отсутствии перехода — `failed` с перечнем доступных | | `ensure_link` | `source`, `target`, `link_type` (логическое имя из `link_types`), `replace` (bool) | `GET /issue/{source}` (`issuelinks`) | нет нужной и нет конфликта — `POST /issueLink`; конфликт (другой тип из `link_types` или обратное направление) и `replace` — `DELETE` лишней, затем `POST`; конфликт без `replace` — `failed` с описанием | | `ensure_sprint` | `key`, `sprint` (имя или `backlog`); `board_id` хаб дописывает при постановке (от клиента — 422) | `GET /issue/{key}` (поле спринта), список спринтов доски `board_id` (активные и будущие) | `POST /rest/agile/1.0/sprint/{id}/issue` или `POST /rest/agile/1.0/backlog/issue` | | `ensure_sprint_meta` | `sprint` (имя), необязательные `rename_to`, `start_date`, `end_date` (`ГГГГ-ММ-ДД`), `goal`; `board_id` хаб дописывает при постановке (от клиента — 422) | спринты доски `board_id` (все состояния) | спринта с именем `sprint` нет — `POST /rest/agile/1.0/sprint` (`originBoardId`, имя `rename_to` или `sprint`, даты, цель); есть — `POST /rest/agile/1.0/sprint/{id}` только с отличающимися полями; спринт закрыт — `failed` «спринт закрыт»; результат `{sprint_id, created}` | | `ensure_sprint_state` | `sprint`, `state` (`active` или `closed`), `move_open_to` (имя активного или будущего спринта той же доски или `backlog`; при `closed` обязателен, при `active` — 422); `board_id` — как у `ensure_sprint_meta` | спринты доски (все состояния), задачи спринта и колонки доски | `active`: спринт будущий — `POST /sprint/{id}` `state = active` (нет дат у спринта — `failed` «у спринта нет дат: задайте ensure_sprint_meta»); `closed`: задачи спринта не в последней колонке доски — в `move_open_to`, затем `state = closed`; уже в нужном состоянии — ничего; результат `{sprint_id, moved: [ключ…]}` | | `ensure_rank` | `key` и ровно одно из `before`, `after` (ключ другой задачи) | ранг обеих задач | `key` уже выше `before` (ниже `after`) — ничего; иначе `PUT /rest/agile/1.0/issue/rank` (`rankBeforeIssue` / `rankAfterIssue`) | | `ensure_attachment` | `key`, `file` (sha256 файла в хранилище хаба, 64 hex-символа), `filename` (имя вложения в Jira, без `/`, `\`, `"` и управляющих символов); `size` хаб дописывает при выдаче | `GET /issue/{key}?fields=attachment` | вложения с тем же `filename` и размером нет — файл у хаба (`GET /agent/files/{file}`), затем `POST /issue/{key}/attachments` (multipart); есть — ничего; результат `attachment_id` | | `refresh` | `keys` (список) или `jql`; `boards` (`[{id, type}]` — доски проектов, `type` — `scrum`, `kanban` или `null`) хаб дописывает при выдаче | `search` с полями зеркала, по 100 на страницу; плюс дочитывание соседей по `issuelinks`; доски с неизвестным типом — `GET /rest/agile/1.0/board/{id}` | ничего в Jira; состояние уходит в хаб; результат — `boards: {"": "scrum" | "kanban" | null}` для досок, чей тип агент запрашивал | | `fetch_attachment` | `key`, `attachment_id` (id вложения Jira, строка из цифр) | `GET /issue/{key}?fields=attachment` (имя и размер вложения) | ничего в Jira; содержимое — `GET /secure/attachment/{id}/{имя}`, затем хабу `POST /agent/files`; результат `{attachment_id, file, size}` | Доску для `ensure_sprint` и для `ensure_fields` с `story_points` хаб SHALL определять при постановке: проект задачи — проекты, чьи метки есть у задачи в зеркале (для временного ключа — в `labels` её `ensure_issue` в пакете, после дописывания метки проекта); `board_id` — доска этого проекта. 422 при постановке: задача ни в одном проекте; в нескольких проектах с разными досками; у проекта нет доски; тип доски `kanban` («у проекта P канбан-доска, спринтов нет»; у `ensure_fields` — «у проекта P канбан-доска, оценка — только на scrum-доске»); тип доски ещё не известен («тип доски проекта P ещё не известен — дождитесь обновления из Jira»). `boards` у `refresh` хаб SHALL дописывать при каждой выдаче (в базе не хранится): все доски проектов без повторов. По результату `refresh` хаб SHALL записать присланные типы `scrum` и `kanban` в проекты с этой доской; `null` тип не меняет. Доску для `ensure_sprint_meta` и `ensure_sprint_state` хаб SHALL определять при постановке по проекту пакета (`project` пакета): пакет без проекта — 422 «команды спринта — только в пакете проекта»; у проекта нет доски, доска `kanban` или тип ещё не известен — 422 с теми же текстами, что у `ensure_sprint`. `ensure_rank` в пакете проекта принадлежит проекту по `key`; `before`/`after` — любая задача. Повтор команды (`command_retry`) SHALL снимать `board_id` у `ensure_sprint_meta`, `ensure_sprint_state` и `ensure_fields`, как у `ensure_sprint`: доска определяется заново. Хвостовой `refresh` пишущего пакета с `ensure_sprint_meta` или `ensure_sprint_state` SHALL быть `refresh {jql: }` (`project = AND labels in ("<метка>")`); иначе — как раньше, по ключам команд. Подтверждения по состоянию зеркала у `ensure_sprint_meta`, `ensure_sprint_state`, `ensure_rank` нет. `ensure_sprint_meta` SHALL искать спринт по имени среди всех спринтов доски: закрытый — `failed` «спринт закрыт»; несколько с этим именем — `failed` «несколько спринтов с именем …: »; `rename_to` на имя другого спринта доски — `failed` «имя занято». `ensure_sprint_state`: `closed` для будущего спринта — `failed` «спринт не начат»; `active` для закрытого — `failed` «спринт закрыт»; результат `ensure_rank` — `{changed}`. Семантика `ensure_link`: `source` блокирует/ссылается на `target`; агент в `POST /issueLink` SHALL ставить `source` в `inwardIssue`, `target` в `outwardIssue` (проверено на Jira банка 11.10.2026). Текст описания (`ensure_fields`) и тело комментария (`ensure_comment`) агент SHALL сравнивать с текущим значением в Jira без различия переводов строк `\r\n` и `\n`; записывается текст команды как есть. Jira хранит присланный текст, а правленный в веб-интерфейсе — с `\r\n` (проверено на Jira банка 13.10.2026). #### Scenario: Повтор уже выполненной команды - **WHEN** `ensure_fields {key: AURORA-1533, duedate: 2026-10-23}` выполняется, а в Jira срок уже 23.10 - **THEN** агент не делает `PUT`, отчёт `done`, `jira_calls = 1` #### Scenario: Описание отличается только переводами строк - **WHEN** `ensure_fields {key: X, description: "а\nб"}`, а в Jira описание X — `"а\r\nб"` - **THEN** агент не делает `PUT`, отчёт `done` с `changed = []` #### Scenario: Замена связи - **WHEN** `ensure_link {source: AURORA-1537, target: AURORA-1538, link_type: "связанные", replace: true}` и в Jira между ними «блокирует начало» 1537→1538 - **THEN** `DELETE` старой связи, пауза по политике, `POST` новой; отчёт `done` с обеими операциями #### Scenario: Связь-конфликт без replace - **WHEN** та же команда с `replace: false` - **THEN** `failed`, в `error` найденная связь, Jira не изменена #### Scenario: Переход по пути - **WHEN** `ensure_status {key: X, target: ["Разработка", "Готово"]}` и X в «Ожидание разработки» - **THEN** два `POST /transitions` с паузой между ними; если X уже «Готово» — `done` без записи #### Scenario: Создание ошибки - **WHEN** `ensure_issue {temp_key: new:1, issue_type: bug, summary: "ОСВ 60 не распознана", labels: [AWPCA]}`, в настройках `issue_types.bug = {name: "Ошибка"}` без `id` - **THEN** агент создаёт задачу с `issuetype = {name: "Ошибка"}`; отчёт `done` с ключом #### Scenario: Вложение уже есть - **WHEN** `ensure_attachment {key: X, file: , filename: "ОСВ 60.xlsx"}`, у X в Jira есть вложение «ОСВ 60.xlsx» того же размера - **THEN** агент не скачивает файл у хаба и не загружает его в Jira; отчёт `done` с `attachment_id` найденного, `jira_calls = 1` #### Scenario: Вложение с тем же именем, другого размера - **WHEN** та же команда, у X есть «ОСВ 60.xlsx» другого размера - **THEN** агент загружает файл вторым вложением с тем же именем (Jira это допускает); отчёт `done` с новым `attachment_id` #### Scenario: Создание с проверкой дубля - **WHEN** `ensure_issue` повторяется после падения агента, а задача с той же темой от нас уже создана час назад - **THEN** агент не создаёт вторую, отчёт `done` с ключом найденной #### Scenario: Спринт задачи проекта - **WHEN** у AURORA-1538 метка `AWPCA`, проект `AWPCA` с доской 4216 типа `scrum`; `ensure_sprint {key: AURORA-1538, sprint: "Спринт 2"}` - **THEN** пакет создан, в параметрах команды `board_id = 4216` #### Scenario: Спринт на канбане - **WHEN** у AURORA-1 метка `STOCKS_DRRK`, доска проекта — `kanban`; `ensure_sprint {key: AURORA-1, sprint: "Спринт 2"}` - **THEN** 422 «у проекта STOCKS_DRRK канбан-доска, спринтов нет» #### Scenario: Тип доски определён - **WHEN** проект `STOCKS_DRRK` с доской 5120 и `board_type = null`; выдаётся `refresh`, агент вернул `boards: {"5120": "kanban"}` - **THEN** при выдаче в параметрах `boards` есть `{id: 5120, type: null}`; после отчёта у проекта `board_type = kanban` #### Scenario: Оценка задачи проекта - **WHEN** у AURORA-1692 метка `AWPCA`, доска проекта 4216 типа `scrum`; `ensure_fields {key: AURORA-1692, story_points: 1}` - **THEN** пакет создан, в параметрах команды `board_id = 4216`; агент читает и пишет оценку через `/rest/agile/1.0/issue/AURORA-1692/estimation?boardId=4216`, `PUT /issue` не делает #### Scenario: Поля без оценки — без доски - **WHEN** `ensure_fields {key: AURORA-1, duedate: 2026-10-30}`, задача вне проектов - **THEN** пакет создан, `board_id` в параметрах нет #### Scenario: Оценка задачи вне проектов - **WHEN** `ensure_fields {key: AURORA-1, story_points: 2}`, у AURORA-1 нет меток проектов - **THEN** 422 «команда 1: AURORA-1 не входит ни в один проект — доску для оценки не определить» #### Scenario: Оценка на канбане - **WHEN** у AURORA-1 метка `STOCKS_DRRK`, доска проекта — `kanban`; `ensure_fields {key: AURORA-1, story_points: 2}` - **THEN** 422 «команда 1: у проекта STOCKS_DRRK канбан-доска, оценка — только на scrum-доске» ### Requirement: Временные ключи и ссылки внутри пакета Параметр-ключ любой команды MAY быть временным `new:`. Хаб SHALL выдавать агенту такую команду только после `done` команды `ensure_issue` с тем же `temp_key` в том же пакете, подставив настоящий ключ. Если `ensure_issue` завершилась `failed` или `cancelled`, зависимые команды SHALL получить `skipped` с ошибкой «создание задачи new: отменено», если `ensure_issue` отменена, иначе «задача new: не создана». Временный ключ вне пакета с его `ensure_issue` SHALL отвергаться при создании пакета (422). #### Scenario: Создать и привязать - **WHEN** пакет: `ensure_issue {temp_key: new:60, …}`, `ensure_link {source: new:60, target: AURORA-1537, …}` - **THEN** вторая команда выдаётся агенту с `source = AURORA-1590` после завершения первой #### Scenario: Создание отменено - **WHEN** пакет: `ensure_issue {temp_key: new:901, …}` (отменена с `force: true`), `ensure_comment {key: new:901, …}` - **THEN** `ensure_comment` — `skipped` с ошибкой «создание задачи new:901 отменено» #### Scenario: Создание упало - **WHEN** `ensure_issue {temp_key: new:902, …}` завершилась `failed`, зависимая `ensure_link {source: new:902, …}` - **THEN** `ensure_link` — `skipped` с ошибкой «задача new:902 не создана» ### Requirement: Пакет и порядок выполнения Пакет SHALL создаваться одним вызовом с автором, заголовком и списком команд; хаб SHALL назначить `seq` по порядку списка и добавить в хвост `refresh` по всем ключам пакета (кроме пакетов, состоящих только из `refresh` и `fetch_attachment`). Хаб SHALL проверять типы параметров: текстовые параметры и ключи — строки (`null` у необязательного параметра — «не задан»; агент трактует его по контракту типа, для `ensure_fields` — очистка поля), `target` команды `ensure_status` — строка или список строк, `labels`, `labels_add`, `labels_remove`, `keys` — списки строк, `replace` — логическое значение; нарушение — 422 с именем параметра. Пакет с двумя и более `ensure_issue` с одинаковой `summary` (после обрезки пробелов по краям) SHALL отвергаться (422). Очередь SHALL выдавать команды в порядке `(batch.created_at, seq)`, одну в полёте. Пакет и его `queued` команды MAY быть отменены; `claimed` команда MUST NOT отменяться автоматически. #### Scenario: Оценка длительности - **WHEN** создаётся пакет из 3 `ensure_fields` и 2 `ensure_comment` - **THEN** `estimate_seconds` = сумма пауз по политике для последовательности типов + пауза `refresh` #### Scenario: Две задачи с одной темой - **WHEN** пакет содержит `ensure_issue {temp_key: new:1, summary: "Отчёт"}` и `ensure_issue {temp_key: new:2, summary: " Отчёт "}` - **THEN** 422 с темой «Отчёт», пакет не создан #### Scenario: Параметр не того типа - **WHEN** пакет содержит `ensure_fields {key: X, labels_add: "AWPCA"}` - **THEN** 422 с указанием `labels_add`, пакет не создан ### Requirement: Политика темпа и пауза на команде Хаб SHALL хранить `pacing` как минимальные зазоры в секундах по типу команды (по умолчанию: `ensure_issue` и `ensure_fields` 120, `ensure_comment`, `ensure_status`, `ensure_link`, `ensure_sprint`, `ensure_sprint_meta`, `ensure_sprint_state`, `ensure_rank`, `ensure_attachment` 30, `refresh` и `fetch_attachment` 2; тип, которого нет в сохранённой настройке `pacing`, получает зазор по умолчанию). При выдаче команды хаб SHALL сообщить `pause_seconds` = зазор по типу выдаваемой команды; агент SHALL выдержать его от времени своего последнего вызова Jira. Внутри одной команды с несколькими записями (`ensure_link` с `replace`, путь переходов) агент SHALL выдерживать зазор типа команды между записями. #### Scenario: Нет следующей команды — нет паузы - **WHEN** очередь пуста - **THEN** `claim` возвращает пустой ответ, агент не спит сверх долгого запроса #### Scenario: Создание спринта повтором не дублируется - **WHEN** пакет проекта `AWPCA` (доска 4216, scrum) `ensure_sprint_meta {sprint: "AWPCA_Sprint_4", start_date: "2026-10-28", end_date: "2026-11-11", goal: "свод ВГО"}` выполнен, затем поставлен повторно - **THEN** первый раз спринт создан (`created: true`), второй — `done` без записи (`created: false`) #### Scenario: Завершение спринта с переносом - **WHEN** `ensure_sprint_state {sprint: "AWPCA_Sprint_3_Corrections", state: "closed", move_open_to: "AWPCA_Sprint_4"}`, в спринте 37 задач, 12 не в последней колонке доски - **THEN** 12 задач перенесены в `AWPCA_Sprint_4`, спринт закрыт; результат `moved` — их ключи; повтор — `done` без записи #### Scenario: Спринт без проекта пакета - **WHEN** `POST /batches` без `project` с `ensure_sprint_meta {sprint: "S"}` - **THEN** 422 «команда 1 (ensure_sprint_meta): команды спринта — только в пакете проекта» #### Scenario: Ранг уже такой - **WHEN** `ensure_rank {key: AURORA-1612, before: AURORA-1601}`, ранг 1612 меньше ранга 1601 - **THEN** `done` без записи в Jira ### Requirement: Ошибки и подтверждение по состоянию Ошибка команды SHALL записываться в `error` (HTTP-код, текст Jira); хаб SHALL хранить не больше 1500 символов `error`, обрезая присланное. Пакет после ошибки продолжается. **Повторная выдача.** Агент запрашивает новую команду, только когда у него нет незавершённой (см. `agent`), поэтому команда в `claimed` к моменту следующего `claim` считается не доставленной или потерянной агентом. При `claim` хаб SHALL сначала выдать снова самую старую (по `claimed_at`, затем `id`) команду в `claimed`: тот же `id` и параметры, `pause_seconds` по текущей политике темпа, новый `claimed_at`, `deliveries + 1`. Если у неё уже `deliveries >= 3`, хаб SHALL перевести её в `failed` с `error = "агент не отчитался после 3 выдач"` и перейти к следующей (довыдача другой `claimed` или следующая `queued`). Довыдача — доставка той же выдачи, а не повтор команды: новых команд хаб MUST NOT создавать сам, повтор после `failed` — новая команда с теми же параметрами по запросу интерфейса. Если агент всё же отчитался `done` с ключом по `ensure_issue`, переведённой в `failed` по пределу, хаб SHALL записать ключ в результат и перенести заметку, не меняя статус (как для отменённой, см. `hub-api`). **Подтверждение по состоянию.** Если после любого `refresh` желаемый результат команды в `claimed` виден в зеркале, хаб SHALL перевести её в `confirmed_by_state`: `ensure_fields` — все поля совпадают (строки сравниваются без различия `\r\n` и `\n`; `null` и пустая строка равны); `ensure_status` — статус равен последнему в пути; `ensure_link` — связь есть и конфликтов нет; `ensure_sprint` — спринт совпадает. `ensure_issue`, `ensure_comment`, `ensure_attachment`, `fetch_attachment` и `refresh` по состоянию не подтверждаются (зеркало не показывает ни загрузку вложения, ни скачанный файл): потерянный отчёт по ним закрывается повторной выдачей (агент находит созданную задачу поиском дубля) или отменой человеком. #### Scenario: Отчёт потерян, факт на месте - **WHEN** `ensure_sprint {key: X, sprint: "Спринт 2"}` в `claimed`, после выполненного `refresh` зеркало показывает X в «Спринт 2» - **THEN** команда `confirmed_by_state` #### Scenario: Описание с CRLF в зеркале - **WHEN** `ensure_fields {key: X, description: "a\nb"}` в `claimed`, после `refresh` в зеркале у X `description = "a\r\nb"` - **THEN** команда `confirmed_by_state` #### Scenario: Ответ на claim потерян - **WHEN** команда 40 выдана (`deliveries = 1`), ответ до агента не дошёл, агент снова прислал `claim` - **THEN** `claim` отдаёт команду 40, `deliveries = 2`; в очереди есть `queued` команды, но выдаётся 40 #### Scenario: Предел выдач - **WHEN** команда 41 в `claimed` с `deliveries = 3`, агент снова прислал `claim`, в очереди есть команда 42 - **THEN** 41 — `failed` «агент не отчитался после 3 выдач»; `claim` отдаёт 42 #### Scenario: Потерянный отчёт о создании задачи - **WHEN** `ensure_issue {temp_key: new:5, summary: "Отчёт"}` выполнена агентом, отчёт не дошёл, агент прислал `claim` - **THEN** команда выдаётся снова; агент находит созданную задачу поиском дубля и отчитывается `done` с её ключом; второй задачи нет, заметка `new:5` перенесена на ключ ### Requirement: Описание параметров и ошибки валидации Описание параметров каждого типа команды (обязательные, необязательные, формат значения, условие на набор) SHALL храниться в коде хаба в одном месте с правилами валидации; описание инструмента MCP `batch_create` SHALL собираться из него (см. `mcp`). Ошибка валидации «нет <параметры>» и «лишние параметры <параметры>» SHALL заканчиваться ожидаемым набором параметров типа: «ожидаются <обязательные>» и, если у типа есть необязательные, «необязательные <список>». #### Scenario: Неверное имя параметра - **WHEN** `POST /batches` с `ensure_status {key: X, status: "Готово"}` - **THEN** 422 «команда 1 (ensure_status): нет target; ожидаются key, target» #### Scenario: Лишний параметр у типа с необязательными - **WHEN** `POST /batches` с `ensure_link {source: X, target: Y, link_type: "связанные", force: true}` - **THEN** 422 «команда 1 (ensure_link): лишние параметры force; ожидаются source, target, link_type; необязательные replace» ### Requirement: Проверки ensure_issue с типом bug и ensure_attachment при постановке При постановке пакета хаб SHALL отвергать 422 без создания пакета: `ensure_issue` с `issue_type = bug`, если в настройках нет `issue_types.bug` с непустым `name` или `id` («нет настройки issue_types.bug»); `ensure_attachment`, если `file` — не 64 hex-символа в нижнем регистре, файла с таким sha256 нет в хранилище хаба («файл не загружен: POST /files»), `filename` пустой (или из одних пробелов) или содержит `/`, `\`, `"` или управляющие символы (код < 32 и 127, в том числе CR и LF): такие имена ломают `multipart/form-data`. Клиент `size` не передаёт (лишний параметр — 422). При выдаче агенту (`claim`, в том числе повторной) хаб SHALL добавить к параметрам `ensure_attachment` `size` — текущий размер файла в хранилище, или `size = null`, если файла уже нет; в базе параметры команды остаются без `size`, поэтому повтор команды (`POST /commands/{id}/retry`) проходит проверку как новая. #### Scenario: Ошибка без настройки типа - **WHEN** `POST /batches` с `ensure_issue {issue_type: bug, …}`, в `issue_types` только `task` и `epic` - **THEN** 422 «команда 1: нет настройки issue_types.bug», пакет не создан #### Scenario: Вложение без загруженного файла - **WHEN** `POST /batches` с `ensure_attachment {key: X, file: <64 hex>, filename: "a.xlsx"}`, файла нет в хранилище - **THEN** 422 «команда 1: файл не загружен: POST /files», пакет не создан #### Scenario: Ошибка с вложением одним пакетом - **WHEN** пакет `[ensure_issue {temp_key: new:1, issue_type: bug, …}, ensure_sprint {key: new:1, sprint: "Спринт 2"}, ensure_attachment {key: new:1, file: , filename: "ОСВ 60.xlsx"}]`, файл загружен - **THEN** пакет создан; `ensure_attachment` выдаётся после завершения `ensure_issue` с подставленным ключом и `size` файла ### Requirement: Пакет проекта Пакет, поставленный с `project = P` (см. `hub-api`), SHALL проходить при постановке, до записи, проверки проекта; любая неудача — 422 с ключом задачи и причиной, пакет не создаётся: - `P` нет среди проектов — 422; - `ensure_issue`: если в `labels` нет P, хаб SHALL дописать P в конец `labels` (в базе параметры хранятся уже с ней; без `labels` — `labels = [P]`); метка, совпадающая с P без учёта регистра, но не точно, SHALL заменяться на P; метка другого проекта в `labels` — 422; - `ensure_fields`: P в `labels_remove` или метка другого проекта в `labels_add` — 422; - `ensure_fields`, `ensure_comment`, `ensure_status`, `ensure_sprint`, `ensure_attachment`: `key` SHALL быть временным ключом `ensure_issue` этого пакета или задачей зеркала с меткой P, иначе 422 « не в проекте P»; - `ensure_link`: хотя бы один из `source`, `target` — временный ключ этого пакета или задача зеркала с меткой P; - `refresh` и `fetch_attachment` — без проверок проекта. «Метка другого проекта» и «P в `labels_remove`» SHALL сравниваться без учёта регистра; принадлежность задачи проекту (метка P у задачи зеркала) — точным совпадением, как в разрезе проекта. Пакет без `project` эти проверки не проходит, метки хаб в нём не дописывает. #### Scenario: Метка проекта дописана - **WHEN** пакет с `project = STOCKS_DRRK`, `ensure_issue {temp_key: new:1, summary: "Сверка остатков", labels: ["kind-dev"]}` - **THEN** пакет создан; в параметрах команды `labels = ["kind-dev", "STOCKS_DRRK"]` #### Scenario: Чужая метка проекта - **WHEN** пакет с `project = STOCKS_DRRK`, `ensure_issue {…, labels: ["awpca"]}`, есть проект `AWPCA` - **THEN** 422 «метка другого проекта: awpca», пакет не создан #### Scenario: Задача другого проекта - **WHEN** пакет с `project = STOCKS_DRRK`, `ensure_fields {key: AURORA-1538, duedate: …}`, у AURORA-1538 метка только `AWPCA` - **THEN** 422 «AURORA-1538 не в проекте STOCKS_DRRK» #### Scenario: Связь с соседом - **WHEN** пакет с `project = STOCKS_DRRK`, `ensure_link {source: AURORA-1 (метка STOCKS_DRRK), target: AURORA-1538 (метка AWPCA), …}` - **THEN** пакет создан ### Requirement: Журнал изменений в refresh Отметка журнала — `started_at` последнего `sync_run` с `history_at IS NOT NULL` (любой `query`). JQL стартовой загрузки — `(<базовая JQL>) AND sprint in openSprints() AND statusCategory != Done`. Хаб SHALL при каждой выдаче `refresh` (первой и повторной) дописывать в параметры `history` (в базе `params` без него, как `boards`): - полный прогон (`refresh` по базовой JQL), отметка есть — `{since: <отметка − 15 минут, ISO-8601 UTC с Z>}`; если `query` прогона-отметки не равна текущей базовой JQL — также `jql: `; - полный прогон, отметки нет — `{jql: }`; - `refresh {keys}` и `refresh` по небазовой JQL — `{since: …}`, если отметка есть; иначе `history` не дописывается; - без базовой JQL (нет проектов) — `history` не дописывается. Результат `refresh` MAY содержать `history` (см. `agent`, «Журнал изменений»); хаб SHALL принимать результат и без него. Отметку (`sync_run.history_at`) хаб SHALL проставлять по правилам `data-model`, «Отметка журнала в журнале синхронизаций». #### Scenario: Первый refresh после выкатки - **WHEN** проект `AWPCA`, прогонов с `history_at` нет; выдаётся полный `refresh` - **THEN** в выданных параметрах `history = {jql: '(project = AURORA AND labels in ("AWPCA")) AND sprint in openSprints() AND statusCategory != Done'}`; в базе `params` без `history` #### Scenario: Регулярный refresh - **WHEN** последний полный прогон с `history_at` начат в `2026-10-22T09:00:00+00:00` по текущей базовой JQL; выдаётся автообновление - **THEN** `history = {since: "2026-10-22T08:45:00Z"}` #### Scenario: Журнал не дочитан - **WHEN** прогон в 10:00 завершился `done` с `history.error`, прогон в 09:00 — с принятым журналом; выдаётся следующий полный `refresh` - **THEN** `history.since` — от прогона 09:00 #### Scenario: Новый проект - **WHEN** прогон с `history_at` в 09:00 по JQL только с `AWPCA`; затем создан проект `STOCKS_DRRK`; выдаётся полный `refresh` - **THEN** `history = {since: "…T08:45:00Z", jql: <стартовая по новой базовой JQL>}` #### Scenario: Хвост пакета - **WHEN** выдаётся хвостовой `refresh {keys: [X]}`, отметка есть - **THEN** `history = {since: …}`; по завершении `done` с `result.history` у его `sync_run` `history_at = NULL` #### Scenario: Хвост пакета до первой отметки - **WHEN** выдаётся хвостовой `refresh {keys: [X]}`, прогонов с `history_at` нет - **THEN** в выданных параметрах нет `history` ### Requirement: Скачивание вложения по запросу `fetch_attachment {key, attachment_id}` SHALL ставиться только хабом по запросу интерфейса (`POST /attachments/{id}/fetch`, см. `hub-api`) — отдельным пакетом из одной команды, без хвостового `refresh` и без проекта (`batch.project = NULL`), заголовок «Скачать вложение ()»; через `POST /batches` её MAY поставить и клиент, с теми же проверками. При постановке хаб SHALL проверять: `attachment_id` — непустая строка из цифр; вложение с этим id есть в зеркале у задачи `key` (иначе 422 «у нет вложения # в зеркале — обновите задачу»); его размер в зеркале не больше 10 МБ (10 485 760 байт) — иначе 413 `too_large` «файл больше 10 МБ (<размер>) — скачайте в Jira»; размер неизвестен (`NULL`) — ставится, предел проверяет агент. Агент SHALL выполнять её так: 1. `GET /issue/{key}?fields=attachment`; вложения с этим id нет — `failed` «вложение # удалено из в Jira»; 2. размер по Jira больше 10 МБ — `failed` «файл больше 10 МБ (<размер>) — скачайте в Jira», содержимое не качается; размер неизвестен — качается, но чтение ответа SHALL обрываться на 10 485 761-м байте с тем же `failed`; 3. содержимое — `GET` по адресу, собранному из `base_url` агента: `/secure/attachment//<имя в URL-кодировке>` (адрес `content` из ответа Jira MUST NOT использоваться: учётные данные уходят только на `base_url`), таймаут не меньше 120 с; ответ Jira 429/503 — повтор по политике агента; перенаправление (3xx) агент MUST NOT выполнять с учётными данными на другой хост (схема, хост и порт не как у `base_url`) — `failed` «Jira перенаправила скачивание на <хост>»; длина полученного не равна известному размеру из шага 1 — `failed` «файл пришёл не целиком (<получено> из <размер>)»; пустое вложение (0 байт) — допустимо; 4. `POST /agent/files {content}` (base64) хабу; ответ `{file, size}`; ошибка хаба — с префиксом «хаб: », недоступность хаба повторяется, как при отправке состояния; 5. результат `done` `{attachment_id, file, size}`. По отчёту `done` хаб SHALL записать скачанный файл вложения (см. `data-model`, «Скачанные вложения»). #### Scenario: Скачивание скриншота - **WHEN** у AURORA-1650 в зеркале вложение #31337 «скрин 1.png» 240 000 байт; поставлена `fetch_attachment {key: AURORA-1650, attachment_id: "31337"}` - **THEN** агент читает задачу, качает `/secure/attachment/31337/%D1%81%D0%BA%D1%80%D0%B8%D0%BD%201.png`, отправляет содержимое хабу; отчёт `done {attachment_id: "31337", file: , size: 240000}`; в хвосте пакета `refresh` нет #### Scenario: Вложение удалили в Jira - **WHEN** вложение #31337 в зеркале есть, в Jira его уже нет - **THEN** команда `failed` «вложение #31337 удалено из AURORA-1650 в Jira»; хаб файл не записывает #### Scenario: Слишком большой файл - **WHEN** в зеркале у вложения #40000 размер 25 000 000 байт; интерфейс просит его скачать - **THEN** 413 `too_large` «файл больше 10 МБ (23,8 МБ) — скачайте в Jira», пакет не создан #### Scenario: Размер неизвестен - **WHEN** у вложения #50 в зеркале `size = NULL`, в Jira размера тоже нет, содержимое — 12 МБ - **THEN** постановка проходит; агент обрывает чтение после 10 МБ, команда `failed` «файл больше 10 МБ (больше 10,0 МБ) — скачайте в Jira»; хабу ничего не отправлено #### Scenario: Перенаправление на другой хост - **WHEN** Jira на `GET /secure/attachment/31337/…` ответила 302 на `https://files.example/…` - **THEN** агент туда не ходит, команда `failed` «Jira перенаправила скачивание на files.example» ### Requirement: Нормализация параметров при постановке При постановке пакета (`POST /batches`, MCP `batch_create`, повтор команды) хаб SHALL сохранять параметры команд в каноническом виде. Нормализация SHALL выполняться сразу после проверки типов значений, до остальных проверок: обязательный параметр, ставший пустым после обрезки, даёт 422 «нет <параметр>»; правила типов, временные ключи `new:` и проверки `ensure_attachment` работают по нормализованным значениям; элемент списка, ставший пустым, даёт 422 «<параметр> — без пустых элементов». Необязательное поле `ensure_fields` из одних пробелов (`priority`, `duedate`, `planned_start`, `planned_end`, `epic_key`, `assignee`) даёт 422 «<параметр> — из одних пробелов; для очистки — "" или null»: после обрезки оно стало бы пустой строкой, которую агент читает как «очистить поле в Jira» (R-8). Строковые параметры обрезаются по краям, кроме текстовых `summary`, `description`, `body` и `goal`, которые хранятся как заданы. Элементы списков `labels`, `labels_add`, `labels_remove` и `keys`, а также `target` у `ensure_status`, если это список, обрезаются поэлементно. Числа, логические значения и `null` не меняются. `filename` у `ensure_attachment` SHALL дополнительно приводиться к нормальной форме Unicode NFC. Агент SHALL получать параметры в том виде, в каком они сохранены. #### Scenario: Ключ с пробелами - **WHEN** `POST /batches` с `ensure_rank {key: "A-1", before: " A-2 "}` - **THEN** пакет создан, параметры команды `{key: "A-1", before: "A-2"}`; агент при `claim` получает `before = "A-2"` #### Scenario: Текст не обрезается - **WHEN** `POST /batches` с `ensure_comment {key: " A-1", marker: "Итог ", body: " отступ\n"}` - **THEN** сохранено `key = "A-1"`, `marker = "Итог"`, `body = " отступ\n"` #### Scenario: Временный ключ с пробелами - **WHEN** пакет `ensure_issue {temp_key: "new:1", …}`, затем `ensure_fields {key: "A-5", epic_key: " new:1 "}` - **THEN** пакет создан, `epic_key` сохранён как `new:1` и подставляется настоящим ключом после выполнения `ensure_issue` #### Scenario: Ключ из пробелов - **WHEN** `POST /batches` с `ensure_status {key: " ", target: "Готово"}` - **THEN** 422 «команда 1 (ensure_status): нет key; ожидаются key, target» #### Scenario: Поле из пробелов не очищает Jira - **WHEN** `POST /batches` с `ensure_fields {key: "A-1", assignee: " "}` - **THEN** 422 «команда 1 (ensure_fields): assignee — из одних пробелов; для очистки — "" или null», пакет не создан #### Scenario: Имя вложения в NFD - **WHEN** `ensure_attachment` с `filename` «отчёт.pdf» в NFD («е» + U+0308) - **THEN** сохранён `filename` «отчёт.pdf» в NFC; повторная постановка того же файла с именем в NFC даёт ту же строку `filename` ### Requirement: Идентификаторы из цифр — только ASCII Хаб SHALL считать строку числовым идентификатором (`attachment_id` в `fetch_attachment`, `id` вложения в состоянии агента, ссылка `#` в MCP, версия агента), только если она непуста и состоит из символов `0-9`. Строка из других цифр Unicode (`"²"`, `"١٢"`) — не идентификатор: при постановке 422, в состоянии агента 422 `bad_state`, в MCP — поиск по имени. Ответ 500 MUST NOT возникать. #### Scenario: Надстрочная цифра - **WHEN** `POST /batches` с `fetch_attachment {key: "A-1", attachment_id: "²"}` - **THEN** 422 «attachment_id — цифры id вложения из issue», пакет не создан