## MODIFIED 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-доске»