Andrey Epifancev b00fa16d54 OpenSpec: R-82 в решения агента v15 (#49)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 19:19:38 +04:00

69 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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=<n>` в строке;
не разобралось — элемент пропускается с записью в лог. Поля нет — `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<смещение>`, смещение — локального времени агента (`time.strftime("%z")`, вид `+0300`).
Сравнение с 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-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` даёт текст «статус <N>» | Текст для этого случая планом не задан; пустой хвост после двоеточия бесполезен | Нет |