Andrey Epifancev 9d3958e337 Сравнивать «готово» при закрытии спринта по id статусов, а не по именам (#49)
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>
2026-10-24 19:42:58 +04:00

18 KiB
Raw Blame History

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<смещение>, смещение — локального времени агента (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), не по смыслу