C-1 финального ревью: ensure_sprint_state closed определял готовые задачи по именам через молча неполный кэш /status и мог необратимо перенести выполненные задачи. Теперь id статусов последней колонки берутся из configuration, у задач — status.id; пустой набор id — failed без записи, задача без status.id считается незавершённой. В board_columns неизвестный id один раз за команду перечитывает /status, затем пишет в лог; колонка без строкового имени пропускается с логом. Решение R-83. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
18 KiB
18 KiB
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.
Решения агента
- Поля 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.
- ранг — поле с
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 (обход активных и будущих спринтов досок), без изменений.rank,story_points— тоже изsearch: id полей добавляются вfields.story_points— число (intилиfloat), иное —null; поле не найдено —story_pointsне передаётся у задач и запись в лог один раз.- Данные досок. Для каждой доски
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не входят.
- колонки —
- Даты команд спринта.
start_date/end_date(ГГГГ-ММ-ДД) →ГГГГ-ММ-ДДT09:00:00.000<смещение>иГГГГ-ММ-ДДT18:00:00.000<смещение>, смещение — локального времени агента (datetime.now().astimezone().strftime("%z"), вид+0300, R-68). Сравнение с Jira — по дате (первые 10 символовstartDate/endDate). 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.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.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}.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), не по смыслу |