49 KiB
commands Specification
Purpose
Контракт команд моста: каждая команда — желаемое состояние одной задачи Jira (ensure_*) или чтение состояния (refresh). Здесь типы команд и их параметры, временные ключи внутри пакета, порядок выполнения, политика темпа, обработка ошибок, повторная выдача и подтверждение по состоянию зеркала.
Requirements
Requirement: Команда — желаемый результат на одной задаче
Каждая команда SHALL описывать желаемое состояние одной задачи Jira и выполняться агентом как «прочитать → сравнить →
записать только разницу». Повторное выполнение любой команды с тем же результатом в Jira SHALL завершаться done
без записи. fetch_attachment в Jira не пишет: она читает файл вложения и передаёт его хабу; повтор безопасен. Типы команд и их контракт:
| Тип | Параметры | Что читает | Что пишет |
|---|---|---|---|
ensure_issue |
temp_key (new:<n>), issue_type (task/epic/bug), summary, description, priority, duedate, planned_start, planned_end, labels, epic_key, assignee |
поиск project = <proj> AND reporter = currentUser() AND created >= -7d, точное сравнение summary |
при отсутствии — POST /issue с типом из issue_types.<issue_type> (для эпика — с 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=<board_id> |
PUT /issue/{key} только с отличающимися полями, кроме оценки; эпик — по epic_link.mode; оценка отличается — PUT /rest/agile/1.0/issue/{key}/estimation?boardId=<board_id> с {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" |
fetch_attachment |
key, attachment_id (id вложения Jira, строка из цифр) |
GET /issue/{key}?fields=attachment (имя и размер вложения) |
ничего в Jira; содержимое — GET <base_url>/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: <JQL проекта пакета>} (project = <project_key> AND labels in ("<метка>")); иначе — как раньше, по ключам команд. Подтверждения по состоянию зеркала у
ensure_sprint_meta, ensure_sprint_state, ensure_rank нет.
ensure_sprint_meta SHALL искать спринт по имени среди всех спринтов доски: закрытый — failed «спринт закрыт»;
несколько с этим именем — failed «несколько спринтов с именем …: <id…>»; 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: <sha>, 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:<n>. Хаб 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и 2ensure_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в зеркале у Xdescription = "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: <sha>, 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:keySHALL быть временным ключом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 стартовой загрузки>; - полный прогон, отметки нет —
{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_runhistory_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 выполнять её так:
GET /issue/{key}?fields=attachment; вложения с этим id нет —failed«вложение # удалено из в Jira»;- размер по Jira больше 10 МБ —
failed«файл больше 10 МБ (<размер>) — скачайте в Jira», содержимое не качается; размер неизвестен — качается, но чтение ответа SHALL обрываться на 10 485 761-м байте с тем жеfailed; - содержимое —
GETпо адресу, собранному изbase_urlагента:<base_url>/secure/attachment/<id>/<имя в URL-кодировке>(адресcontentиз ответа Jira MUST NOT использоваться: учётные данные уходят только наbase_url), таймаут не меньше 120 с; ответ Jira 429/503 — повтор по политике агента; перенаправление (3xx) агент MUST NOT выполнять с учётными данными на другой хост (схема, хост и порт не как уbase_url) —failed«Jira перенаправила скачивание на <хост>»; длина полученного не равна известному размеру из шага 1 —failed«файл пришёл не целиком (<получено> из <размер>)»; пустое вложение (0 байт) — допустимо; POST /agent/files {content}(base64) хабу; ответ{file, size}; ошибка хаба — с префиксом «хаб: », недоступность хаба повторяется, как при отправке состояния;- результат
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 агент читает задачу, качает
<base_url>/secure/attachment/31337/%D1%81%D0%BA%D1%80%D0%B8%D0%BD%201.png, отправляет содержимое хабу; отчётdone {attachment_id: "31337", file: <sha256>, 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 <base_url>/secure/attachment/31337/…ответила 302 наhttps://files.example/… - THEN агент туда не ходит, команда
failed«Jira перенаправила скачивание на files.example»
Requirement: Нормализация параметров при постановке
При постановке пакета (POST /batches, MCP batch_create, повтор команды) хаб SHALL сохранять параметры команд в
каноническом виде. Нормализация SHALL выполняться сразу после проверки типов значений, до остальных проверок:
обязательный параметр, ставший пустым после обрезки, даёт 422 «нет <параметр>»; правила типов, временные ключи
new:<n> и проверки 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 вложения в состоянии
агента, ссылка #<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», пакет не создан