# agent-v15-sprints — решения агента Спека и дизайн изменения — в `sdlc-hub/openspec/changes/sprints-backlog/` (issue aep/sdlc-hub#108, aep/jira-agent#49). Агентская часть: требование `agent` «Спринты, ранг и оценки» (ADDED); контракт команд — `commands` «Команда — желаемый результат на одной задаче» (MODIFIED: `story_points` в `ensure_fields`, `ensure_sprint_meta`, `ensure_sprint_state`, `ensure_rank`) и абзацы о командах спринтов; формат состояния — `hub-api` «Приём досок и спринтов» (ADDED). Версия агента — 15. ## Решения агента 1. **Поля Jira — по типу поля, один раз на процесс.** `GET /rest/api/2/field` при первой нужде, кэш на процесс: - ранг — поле с `schema.custom = "com.pyxis.greenhopper.jira:gh-lexo-rank"`; - спринт — поле с `schema.custom = "com.pyxis.greenhopper.jira:gh-sprint"`; - story points — поле с именем «Story Points» (без учёта регистра); несколько — первое по id. Перекрытие из настроек хаба: `field_ids.rank`, `field_ids.sprint`, `field_ids.story_points` (если заданы — поиск не нужен). Ошибка `GET /field` — поля считаются ненайденными, причина в лог; повтор — в следующем `refresh`. 2. **`sprint_ids` — из поля спринта в том же `search`**, а не обходом спринтов: id поля спринта добавляется в `fields` поиска. Значение — список; элемент — объект с `id` (Jira 8+) или строка `com.atlassian.greenhopper.service.sprint.Sprint@…[id=72,…]` (Jira Server): id — из `id` или из `id=` в строке; не разобралось — элемент пропускается с записью в лог. Поля нет — `sprint_ids` не передаётся (хаб сохранит прежнее). Имя текущего спринта (`sprint`) — как в v12 (обход активных и будущих спринтов досок), без изменений. 3. **`rank`, `story_points` — тоже из `search`**: id полей добавляются в `fields`. `story_points` — число (`int` или `float`), иное — `null`; поле не найдено — `story_points` не передаётся у задач и запись в лог один раз. 4. **Данные досок.** Для каждой доски `scrum`/`kanban` из `refresh.boards` (тип известный или только что определённый): - колонки — `GET /rest/agile/1.0/board/{id}/configuration` → `columnConfig.columns[]` `{name, statuses: [{id}]}`; id статусов переводятся в имена по `GET /rest/api/2/status` (кэш на процесс; неизвестный id — пропускается); - kanban-бэклог (только `kanban`): `GET /rest/agile/1.0/board/{id}/backlog?maxResults=0` — 200 → `true`, 400 → `false`, иное → `null`; проверить на Jira банка при выкатке; - спринты (только `scrum`): `GET /rest/agile/1.0/board/{id}/sprint` без фильтра состояния, постранично (50); элемент → `{id, name, state, start_date: startDate, end_date: endDate, complete_date: completeDate, goal}`; - ошибка чтения настроек доски — `columns: null`, `kanban_backlog: null`, спринты — если прочитались; причина в лог. `boards` прикладывается к первой пачке `POST /agent/state`; при пустой выборке — одна пачка `{issues: [], links: [], boards}`. Доски без данных (тип `null`) в `boards` не входят. 5. **Даты команд спринта.** `start_date`/`end_date` (`ГГГГ-ММ-ДД`) → `ГГГГ-ММ-ДДT09:00:00.000<смещение>` и `ГГГГ-ММ-ДДT18:00:00.000<смещение>`, смещение — локального времени агента (`datetime.now().astimezone().strftime("%z")`, вид `+0300`, R-68). Сравнение с Jira — по дате (первые 10 символов `startDate`/`endDate`). 6. **`ensure_sprint_meta`.** Спринты доски `board_id` — все состояния (тот же обход, что в п. 4, кэш на команду); поиск по имени `sprint` (точное совпадение). Нет — `POST /rest/agile/1.0/sprint` `{name: rename_to or sprint, originBoardId, startDate?, endDate?, goal?}`; есть — `POST /rest/agile/1.0/sprint/{id}` только с отличающимися полями (`name` при `rename_to`, даты по п. 5, `goal`); нет отличий — `done` без записи. Ошибки — по контракту `commands`. 7. **`ensure_sprint_state`.** `active`: спринт `future` — `POST /sprint/{id}` `{state: "active", startDate, endDate}` (из спринта; нет дат — `failed`). `closed`: колонки доски (п. 4) → набор статусов последней колонки; задачи спринта (`GET /sprint/{id}/issue?fields=status`, постранично) не в этих статусах — в `move_open_to` (`POST /sprint/{to}/issue` или `POST /backlog/issue`, по 50 ключей), затем `POST /sprint/{id}` `{state: "closed"}`. Колонки не прочитались — `failed` «не удалось прочитать колонки доски». Между записями — зазор типа команды, как в `ensure_link`. 8. **`ensure_rank`.** Ранг обеих задач — `search` по `key in (key, other)` с полем ранга; нет поля ранга — `failed` «поле ранга не найдено»; уже выше/ниже (сравнение строк) — `done {changed: false}`; иначе `PUT /rest/agile/1.0/issue/rank` `{issues: [key], rankBeforeIssue | rankAfterIssue: other, rankCustomFieldId}` → `{changed: true}`. 9. **`story_points` в `ensure_fields`.** Поле по п. 1; не найдено — `failed` «поле оценки не найдено в Jira: задайте field_ids.story_points». Сравнение чисел: равны, если `abs(a - b) < 1e-9`; `null` — очистка (`None` в `PUT`). ## Decisions made without discussion | Маркер | Дата | Решение | Обоснование | Цена ошибки | |---|---|---|---|---| | R-60 | 2026-10-24 | Поля ранга, спринта, оценки — по типу/имени через `GET /field`, перекрытие `field_ids` (п. 1) | Владелец полей не знает, оценок в AURORA ещё нет | Не то поле «Story Points» — задать `field_ids.story_points` | | R-61 | 2026-10-24 | `sprint_ids` из поля спринта в `search`, разбор строкового вида Jira Server (п. 2) | Обход закрытых спринтов — N запросов на спринт; поле есть у каждой задачи | Формат поля банка иной — `sprint_ids` пропадут, видно на живой проверке | | R-62 | 2026-10-24 | Kanban-бэклог — по ответу `GET /board/{id}/backlog` (п. 4) | В `configuration` Jira Server явного признака нет | `null` — у kanban нет раздела «Бэклог» | | R-63 | 2026-10-24 | Время дат спринта — 09:00 и 18:00 по локальному времени агента (п. 5) | Команда несёт только дату | Другое время в Jira — правка спринта руками | | R-64 | 2026-10-24 | Поле ранга/спринта/оценки не найдено — ключ `rank`/`sprint_ids`/`story_points` у задач не передаётся (design агента 2–3), а не `null` (спека `agent`: «значение null у всех задач») | Сбой `GET /field` с `null` стёр бы значения в зеркале; хаб без поля сохраняет прежнее, у новой базы прежнее — `NULL` | Поле удалили в Jira — в зеркале остаются старые значения до ручной правки | | R-65 | 2026-10-24 | Ошибка чтения спринтов доски — доска не входит в `boards` (хаб сохраняет прежние данные), причина в лог; ошибка чтения настроек при прочитанных спринтах — доска с `columns: null` | Доска из `boards` заменяет все свои спринты: `sprints: []` стёр бы историю; спека «прежние правила v12 … refresh продолжается» внутренне противоречива | Данные доски устаревают до следующего удачного `refresh` | | R-66 | 2026-10-24 | Пустая пачка с `boards` — только когда в параметрах есть `boards` (хаб v15) и выборка пуста (`jql` без задач, `keys: []`); все ключи 400 — по-прежнему `failed` (v14, `test_all_keys_400_fails`); `pages` считает и пустую пачку | Без `boards` пустая пачка бесполезна; «ни один ключ не найден» — признак ошибки конфигурации (R-7 v4) | Хвост `refresh` по ключам удалённых задач — `failed`, данные досок в этот раз не обновятся | | R-67 | 2026-10-24 | Любой `HttpError` на `GET /field` — поля не найдены до конца команды, следующая команда спрашивает снова; `ConnectionError` — как везде (повтор `refresh`); список полей один на команду и общий с журналом изменений (`field_id_of`) | design агента 1; один `GET /field` за `refresh` сохраняет `test_custom_field_lookup_once` | Нет | | R-68 | 2026-10-24 | Смещение дат спринта — `datetime.now().astimezone().strftime("%z")`, не `time.strftime("%z")` (design агента 5) | `time.strftime("%z")` на Windows отдаёт имя пояса, а не `+0300` | Нет | | R-69 | 2026-10-24 | `ensure_sprint_meta`: спринта `sprint` нет, а ровно один не закрытый спринт с именем `rename_to` есть — это он (повтор после переименования), правятся его поля; есть оба — «имя занято»; «несколько с именем» проверяется и для `rename_to` | Повтор команды не должен падать или создавать второй спринт (Anti-Goal proposal) | Нельзя создать новый спринт с именем существующего через `rename_to` — и не нужно | | R-70 | 2026-10-24 | У `ensure_sprint_meta` и `ensure_sprint_state` `board_id` только из команды, без запасного `board_id` настроек; нет — `failed` «board_id нет в команде — спринты недоступны» | Новые типы ставит только хаб v15, он всегда дописывает доску; чужая доска из настроек опаснее отказа | Нет | | R-71 | 2026-10-24 | Правило «несколько спринтов с именем» — и для `ensure_sprint_state`; `move_open_to` — активный или будущий спринт той же доски, не сам закрываемый, ровно один с этим именем; все проверки — до первой записи; спринт уже в нужном состоянии — `done` без проверки `move_open_to` | Повтор безопасен; ошибка параметров не оставляет спринт полузакрытым | Повтор с опечаткой в `move_open_to` после закрытия — `done`, опечатка не замечена | | R-72 | 2026-10-24 | Спринт доски без `id`/`name` или с состоянием вне `active`/`future`/`closed` (после `lower()`) — отбрасывается с записью в лог | Хаб отвергает всю пачку (`bad_state`), `refresh` упал бы целиком | Такой спринт не виден в вебе | | R-73 | 2026-10-24 | Ответ `PUT /issue/rank` с `entries[].status` не 2xx — `failed` «Jira не переранжировала <ключ>: » | Jira Agile сообщает о частичном отказе кодом 207 | Нет | | R-74 | 2026-10-24 | Порядок кандидатов полей `(len(id), id)` — числовой порядок `customfield_N`, для всех трёх полей | design агента 1 («первое по id») задан только для оценки, строковое сравнение ставит `customfield_9999` после `customfield_10050` | Не то поле — перекрытие `field_ids` | | R-75 | 2026-10-24 | Кэш полей и имён статусов — на экземпляр `Executor` (Runner пересоздаёт его при смене настроек) | «Один раз на процесс» с учётом смены `field_ids` | Нет | | R-76 | 2026-10-24 | Трейлер `Co-Authored-By` в коммитах — по плану («Claude Opus 5.5»), а не по системной подсказке сессии | План специфичнее общей подсказки | Другое имя модели в трейлере коммитов | | R-77 | 2026-10-24 | Поиск полей: оценка — только по имени «Story Points», ранг и спринт — только по `schema.custom`; при `HttpError` на `GET /field` частичное перекрытие `field_ids` сохраняется; полное перекрытие `GET /field` не делает | Решения исполнителя Task 1, согласуются с п. 1 и R-67 | Поле оценки названо иначе — задать `field_ids.story_points` | | R-78 | 2026-10-24 | Решения исполнителя Task 3: при пустых `keys` пустая пачка с `boards` уходит внутри раннего return (журнал изменений пропускается, как в v14); отброшенный спринт в логе назван по id; элементы `/status` без id/name пропускаются; в кэш `_sprints` пишутся только активные и будущие | Согласуется с брифом, R-65, R-66, R-72 и поведением v14 | Нет | | R-79 | 2026-10-24 | `ensure_fields`: настроенный `field_ids.story_points` читается напрямую, без `jira_fields()`; значение `story_points` проверяется до `GET /issue` | Результат совпадает с `jira_fields`; при частичном перекрытии не делается лишний `GET /field` (тест `test_story_points_override`) | Нет | | R-80 | 2026-10-24 | `ensure_sprint_meta`: `goal` не передан — цель не трогается (очистка — `goal: ""`); `rename_to` равный `sprint` считается отсутствующим; «имя занято» и повтор после переименования учитывают только не закрытые спринты с именем `rename_to` (дубли `sprint` — спринты любых состояний); порядок проверок: даты → доска → дубли → `rename_to` → «спринт закрыт» → запись | Решения исполнителя Task 5; нужны идемпотентность повтора (R-69) и отсутствие записи до проверок | Хаб ждёт очистки цели через `null` — цель не очистится, передавать `""` | | R-81 | 2026-10-24 | `ensure_sprint_state`: задача без статуса в ответе — незавершённая; дубликаты имени цели переноса — ошибка «несколько спринтов с именем…»; зазор `gap` ставится и перед закрытием спринта, если до этого были переносы | Решения исполнителя Task 6; безопаснее перенести лишнее, чем потерять незавершённое | Задача без статуса, на деле готовая, уедет в следующий спринт | | R-82 | 2026-10-24 | `ensure_rank`: элемент `entries` с неуспешным статусом без `errors` даёт текст «статус » | Текст для этого случая планом не задан; пустой хвост после двоеточия бесполезен | Нет | | R-83 | 2026-10-24 | `ensure_sprint_state`: «готово» определяется по id статусов последней колонки (из configuration) и `status.id` задач, а не по именам; пустой набор id — `failed`; в `refresh` неизвестный id статуса → один раз перечитать `/status`, затем лог | Необратимый перенос готовых задач при неполном/устаревшем кэше имён (C-1 финального ревью) | Ещё один `GET /status` при неизвестном id; отступление от п. 7 design по способу (имена → id), не по смыслу |