# commands Specification ## Purpose Контракт команд моста: каждая команда — желаемое состояние одной задачи Jira (`ensure_*`) или чтение состояния (`refresh`). Здесь типы команд и их параметры, временные ключи внутри пакета, порядок выполнения, политика темпа, обработка ошибок, повторная выдача и подтверждение по состоянию зеркала. ## Requirements ### Requirement: Команда — желаемый результат на одной задаче Каждая команда SHALL описывать желаемое состояние одной задачи Jira и выполняться агентом как «прочитать → сравнить → записать только разницу». Повторное выполнение любой команды с тем же результатом в Jira SHALL завершаться `done` без записи. Типы команд и их контракт: | Тип | Параметры | Что читает | Что пишет | |---|---|---|---| | `ensure_issue` | `temp_key` (`new:`), `issue_type` (`task`/`epic`), `summary`, `description`, `priority`, `duedate`, `planned_start`, `planned_end`, `labels`, `epic_key`, `assignee` | поиск `project = AND reporter = currentUser() AND created >= -7d`, точное сравнение `summary` | при отсутствии — `POST /issue` (для эпика — с `epic_name_field_id`); результат `key` | | `ensure_fields` | `key`, любое подмножество: `summary`, `description`, `priority`, `duedate`, `planned_start`, `planned_end`, `labels_add`, `labels_remove`, `epic_key`, `assignee` | `GET /issue/{key}` | `PUT /issue/{key}` только с отличающимися полями; эпик — по `epic_link.mode` | | `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`) | `GET /issue/{key}` (поле спринта), список спринтов доски из `board_id` (активные и будущие) | `POST /rest/agile/1.0/sprint/{id}/issue` или `POST /rest/agile/1.0/backlog/issue` | | `refresh` | `keys` (список) или `jql` | `search` с полями зеркала, по 100 на страницу; плюс дочитывание соседей по `issuelinks` | ничего в Jira; состояние уходит в хаб | Семантика `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` повторяется после падения агента, а задача с той же темой от нас уже создана час назад - **THEN** агент не создаёт вторую, отчёт `done` с ключом найденной ### 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`). Хаб 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` 30, `refresh` 2). При выдаче команды хаб SHALL сообщить `pause_seconds` = зазор по типу выдаваемой команды; агент SHALL выдержать его от времени своего последнего вызова Jira. Внутри одной команды с несколькими записями (`ensure_link` с `replace`, путь переходов) агент SHALL выдерживать зазор типа команды между записями. #### Scenario: Нет следующей команды — нет паузы - **WHEN** очередь пуста - **THEN** `claim` возвращает пустой ответ, агент не спит сверх долгого запроса ### 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` и `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` перенесена на ключ