diff --git a/docs/superpowers/plans/2026-10-25-sp-estimation-board.md b/docs/superpowers/plans/2026-10-25-sp-estimation-board.md new file mode 100644 index 0000000..11aa7a5 --- /dev/null +++ b/docs/superpowers/plans/2026-10-25-sp-estimation-board.md @@ -0,0 +1,61 @@ +# Оценка через доску (хаб) Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** хаб дописывает `board_id` доски проекта задачи в `ensure_fields` со `story_points`, чтобы агент v16 ставил оценку через доску. + +**Architecture:** `hooks._assign_boards` уже ставит доску в `ensure_sprint` по меткам задачи; расширяем его на +`ensure_fields` с ключом `story_points` (свои тексты 422 для канбана и «вне проектов»). `retry_params` снимает +`board_id` и у `ensure_fields`. Описание `batch_create` в MCP — одна фраза. + +**Tech Stack:** Python 3.13, pytest. + +**Spec:** `openspec/changes/sp-estimation-board/` (proposal, design, specs/commands, specs/mcp). + +## Global Constraints + +- От клиента `board_id` в `ensure_fields` — 422 «лишние параметры board_id» (валидация уже такая: `board_id` не в списке параметров — не добавлять его в `commands.COMMANDS`). +- Тексты 422 (команда N — как у `ensure_sprint`): «команда N: K не входит ни в один проект — доску для оценки не определить»; «команда N: у проекта P канбан-доска, оценка — только на scrum-доске»; нет доски, тип не известен, разные доски — тексты `ensure_sprint` без изменений. +- `ensure_fields` без `story_points` — без доски и без проверок доски. + +## Review Focus + +1. `ensure_fields {story_points}` для временного ключа `new:` — доска по меткам его `ensure_issue` в пакете (как у спринта) — тест в Task 1. +2. `ensure_fields` без оценки на задаче вне проектов — пакет создаётся, `board_id` нет — тест в Task 1. +3. Повтор `ensure_fields` со `story_points` после смены доски проекта — новая доска — тест в Task 1. +4. Пакет из `ensure_fields` и `ensure_sprint` на одну задачу — обе получают доску — тест в Task 1. +5. Повтор `ensure_fields` без оценки — параметры без изменений — тест в Task 1. + +--- + +### Task 1: Доска в ensure_fields со story_points + +**Files:** +- Modify: `src/sdlc_hub/modules/jira/hooks.py` (`_assign_boards`, `retry_params`) +- Test: `tests/test_queue.py` (рядом с `test_sprint_gets_project_board`) + +**Interfaces:** +- Produces: в `params` `ensure_fields` со `story_points` — `board_id: int`. + +- [ ] **Step 1: Тесты** (`fields_sp(key, sp=1)` → `{"type": "ensure_fields", "params": {"key": key, "story_points": sp}}`): + - `test_story_points_gets_project_board` — AURORA-1692 с меткой `AWPCA`: params `== {"key": "AURORA-1692", "story_points": 1, "board_id": 4216}`; + - `test_story_points_board_rejected` — parametrize как `test_sprint_board_rejected`, но канбан → «у проекта STOCKS_DRRK канбан-доска, оценка — только на scrum-доске», вне проектов → «AURORA-77 не входит ни в один проект — доску для оценки не определить»; батчей 0; + - `test_fields_without_points_no_board` — `ensure_fields {key: AURORA-77, duedate: "2026-10-30"}` вне проектов: пакет создан, `"board_id" not in params`; + - `test_story_points_for_new_issue_uses_batch_labels` — `ensure_issue new:1` в пакете проекта `AWPCA` + `fields_sp("new:1")` → `board_id == 4216`; + - `test_fields_and_sprint_same_issue` — `[fields_sp("A-1"), sprint("A-1")]`: у обеих `board_id == 4216`; + - `test_retry_story_points_recomputes_board` — как `test_retry_sprint_recomputes_board`: после смены доски на 5120 повтор → 5120; + - `test_retry_fields_without_points_keeps_params` — повтор `ensure_fields {key, duedate}` → params те же. +- [ ] **Step 2:** `uv run pytest tests/test_queue.py -k "story_points or fields_" -q` — FAIL. +- [ ] **Step 3:** `_assign_boards`: обрабатывать `ensure_sprint` и `ensure_fields` с `"story_points" in params`; слово цели («спринта»/«оценки») и текст канбана — по типу команды; `_board_error` получает необязательный текст канбана (`kanban_text`), по умолчанию прежний. `retry_params`: `drop` и для `ensure_fields`. Docstring `_assign_boards` — упомянуть оценку. +- [ ] **Step 4:** `uv run pytest -q` — весь набор зелёный. +- [ ] **Step 5:** commit «Доска проекта в ensure_fields со story_points (#113)». + +### Task 2: Описание batch_create в MCP + +**Files:** +- Modify: `src/sdlc_hub/modules/jira/mcp.py:500` +- Test: `tests/test_mcp.py:224` + +- [ ] **Step 1:** в тесте строка `"board_id для ensure_sprint, ensure_sprint_meta и ensure_sprint_state выставляет хаб"` → `"board_id для ensure_sprint, ensure_sprint_meta, ensure_sprint_state и ensure_fields со story_points выставляет хаб"`; FAIL. +- [ ] **Step 2:** та же фраза в описании `batch_create`; `uv run pytest -q` и `make check` — зелёные. +- [ ] **Step 3:** commit «MCP: board_id в ensure_fields со story_points ставит хаб (#113)». diff --git a/openspec/changes/sp-estimation-board/.openspec.yaml b/openspec/changes/sp-estimation-board/.openspec.yaml new file mode 100644 index 0000000..7ccf7c3 --- /dev/null +++ b/openspec/changes/sp-estimation-board/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-10-25 diff --git a/openspec/changes/sp-estimation-board/design.md b/openspec/changes/sp-estimation-board/design.md new file mode 100644 index 0000000..ef2551a --- /dev/null +++ b/openspec/changes/sp-estimation-board/design.md @@ -0,0 +1,51 @@ +## Context + +Основа — `main` `af70ba8` (после sprints-backlog #108, схема v9, агент v15). Issue #113, ветка +`feat/113-sp-estimation-board`. Агент — jira-agent#53, ветка `feat/53-v16-sp-estimation`, design-only change +`agent-v16-sp-estimation`. + +Что есть сейчас: +- агент v15 пишет `story_points` в `PUT /rest/api/2/issue/{key}` вместе с остальными полями (`do_ensure_fields`); +- Jira банка 25.10.2026 отклонила запись: поле оценки не на экране редактирования; +- хаб дописывает `board_id` только в `ensure_sprint` (по проекту задачи, `hooks._assign_boards`) и в команды спринта + (по проекту пакета, `hooks._assign_project_board`); `retry_params` снимает его у этих типов; +- живые данные: доска 4216 (`AWPCA`, scrum), поле оценки доски `customfield_15852` = `field_ids.story_points` хаба. + +## Goals / Non-Goals + +**Goals:** оценки из чата ставятся в Jira банка; повтор без изменений — без записи; контракт `ensure_fields` для +остальных полей не меняется. + +**Non-Goals:** см. proposal. + +## Decisions + +1. **Оценка — через ресурс оценки доски** `/rest/agile/1.0/issue/{key}/estimation?boardId=…` (владелец, 25.10.2026: + «давай» на предложенную схему после проверки ресурса в браузере). Так ставит оценку бэклог Scrum; экран + редактирования не нужен. +2. **Доска — по проекту задачи, как у `ensure_sprint`** (владелец, 25.10.2026). Те же 422; текст для канбана — + свой: «у проекта P канбан-доска, оценка — только на scrum-доске»; задача вне проектов — «… доску для оценки не + определить». +3. **`board_id` только при `story_points` в параметрах.** `ensure_fields` без оценки проверок доски не проходит — задачи + вне проектов правятся, как раньше. +4. **Повтор (`command_retry`) снимает `board_id` у `ensure_fields`** — доска определяется заново, как у `ensure_sprint`. +5. **Подтверждение по зеркалу не меняется:** `story_points` в зеркале — то же поле `customfield_15852`. + +## Risks / Trade-offs + +- **Формат `value` в `PUT …/estimation`.** Документация Atlassian показывает строку (`"8.5"`), Jira отдаёт число + (`1.0`). Агент шлёт число; если Jira банка не примет — правка агента после живой проверки на одной задаче (до пакета). +- **Частичная запись.** Остальные поля `PUT /issue` пишутся до оценки; ошибка оценки — `failed`, повтор команды + остальные поля не меняет (уже совпадают). + +## Decisions made without discussion + +| Дата | Решение | Обоснование | Цена ошибки | +|---|---|---|---| +| 25.10.2026 | R-1. Без `board_id` в команде агент v16 ставит оценку через `PUT /issue`, как v15 | совместимость с хабом до этого change; агент выкатывается после хаба, но порядок не гарантирован | нет: без доски поведение то же, что сейчас | +| 25.10.2026 | R-2. Порядок в агенте: сначала `PUT /issue` с остальными полями, затем оценка | оценка — новый путь с риском ошибки; остальные поля не должны зависеть от неё | частичная запись при ошибке оценки — повтор безопасен | +| 25.10.2026 | R-3. `GET /issue` не делается, если в команде только оценка | пакет оценок — 64 команды; минус один вызов Jira на команду | нет | +| 25.10.2026 | R-4. Текст 422 для канбана у `ensure_fields` — отдельный | «спринтов нет» вводит в заблуждение для оценки | косметика | +| 25.10.2026 | R-5. `value` в `PUT …/estimation` — число (`null` — очистить) | Jira отдаёт значение числом; проверяется вживую до пакета | одна правка агента | +| 25.10.2026 | I-1 (финальное ревью): между `PUT /issue` и `PUT …/estimation` одной команды — зазор `ensure_fields` | политика темпа: зазор между записями внутри команды | пакет «оценка + поля» идёт медленнее | +| 25.10.2026 | I-2 (финальное ревью): `fieldId` оценки доски сверяется с `field_ids.story_points` (если задан), расхождение — `failed` без записи | иначе число уйдёт в чужое поле доски другого проекта | лишний отказ, если поле настроено неверно — правится настройкой | diff --git a/openspec/changes/sp-estimation-board/proposal.md b/openspec/changes/sp-estimation-board/proposal.md new file mode 100644 index 0000000..390577b --- /dev/null +++ b/openspec/changes/sp-estimation-board/proposal.md @@ -0,0 +1,33 @@ +## Why + +25.10.2026 первый пакет оценок (64 команды `ensure_fields` со `story_points` из сессии awp-ca) остановился на первой же +команде: Jira банка отвечает на `PUT /rest/api/2/issue/{key}` «Field cannot be set. It is not on the appropriate screen, +or unknown». Поле оценки `customfield_15852` («Story point estimate») верное, но его нет на экране редактирования: +в Jira оценку можно поставить только из бэклога Scrum. Владелец проверил в браузере: в Jira банка есть +`GET /rest/agile/1.0/issue/AURORA-1704/estimation?boardId=4216` → `{"fieldId":"customfield_15852","value":1.0}`, а у доски +4216 в настройках `estimation.field.fieldId = customfield_15852`. Этот же ресурс принимает `PUT` и пишет оценку в поле +доски без проверки экрана. + +## What Changes + +- **Команды:** `ensure_fields` со `story_points` получает `board_id` доски проекта задачи — хаб дописывает при + постановке по тем же правилам, что у `ensure_sprint` (422: задача вне проектов, разные доски, нет доски, канбан, тип + не известен); от клиента `board_id` — 422; `command_retry` снимает `board_id` и определяет доску заново. +- **Агент v16** (design-only change `agent-v16-sp-estimation` в jira-agent, issue jira-agent#53): оценка из + `ensure_fields` с `board_id` — через `GET`/`PUT /rest/agile/1.0/issue/{key}/estimation?boardId=…`; остальные поля — + как раньше; без `board_id` — как v15. +- **MCP:** описание `batch_create` сообщает, что `board_id` у `ensure_fields` со `story_points` хаб дописывает сам. + +## Non-Goals + +- Оценки на канбан-досках: команда отклоняется при постановке (решение владельца 25.10.2026; канбан-проектов с оценками пока нет). +- Чтение оценки в `refresh` через доску: поле `field_ids.story_points = customfield_15852` верное, чтение не меняется. +- Схема зеркала, API чтения, веб — не меняются. + +## Impact + +- `src/sdlc_hub/modules/jira/hooks.py` (`_assign_boards`, `retry_params`), `src/sdlc_hub/modules/jira/mcp.py` (описание + `batch_create`), тесты `tests/test_api.py`, `tests/test_commands.py`/`tests/test_mcp.py`. +- Агент: `jira-agent/src/agent.py` (`do_ensure_fields`, `AGENT_VERSION = 16`). +- Выкатка: хаб (pull, kickstart, `/mcp` reconnect) → `agent publish` v16 → живая проверка оценки одной задачи → + `min_agent_version = 16` → пакет оценок заново. diff --git a/openspec/changes/sp-estimation-board/specs/agent/spec.md b/openspec/changes/sp-estimation-board/specs/agent/spec.md new file mode 100644 index 0000000..ea0c7cd --- /dev/null +++ b/openspec/changes/sp-estimation-board/specs/agent/spec.md @@ -0,0 +1,53 @@ +## MODIFIED Requirements + +### Requirement: Спринты, ранг и оценки +Агент v15 SHALL в каждом `refresh` (по JQL и по ключам) для каждой доски из `boards` команды с типом `scrum` или +`kanban` (известным или только что определённым) прочитать настройки доски (`GET /rest/agile/1.0/board/{id}/configuration`: +колонки и их статусы — по именам статусов; признак kanban-бэклога, если Jira его отдаёт, иначе `null`) и, у `scrum`, +все спринты доски любого состояния (`GET /rest/agile/1.0/board/{id}/sprint` постранично) и приложить к первой пачке +состояния `boards` (см. `hub-api`, «Приём досок и спринтов»). Ошибка чтения настроек доски — доска с `columns: null`, +`kanban_backlog: null`; ошибка чтения списка спринтов scrum-доски — доска в `boards` не входит (хаб сохраняет прежние +данные), имена спринтов задач — по правилам v12; причина в лог; `refresh` продолжается. + +Каждая задача в состоянии (с v15) SHALL нести `rank` — значение поля ранга Jira (поле типа LexoRank), `story_points` — +значение поля оценки (`field_ids.story_points`, если задано, иначе поле Jira с именем «Story Points»), и `sprint_ids` — +id всех спринтов задачи из поля спринта Jira (любого состояния). Поле не найдено — ключ у задач не передаётся (хаб +сохраняет прежнее значение) и запись в лог один раз за `refresh`. + +Агент v15 SHALL выполнять `ensure_sprint_meta`, `ensure_sprint_state`, `ensure_rank` и `story_points` в +`ensure_fields` по контракту `commands`; сравнение — до записи, повтор без изменений — `done` без записи. + +Агент v16 SHALL ставить `story_points` из `ensure_fields` с `board_id` через оценку доски, а не через `PUT /issue`: поле +оценки может не быть на экране редактирования, и Jira отвечает «Field cannot be set. It is not on the appropriate screen, +or unknown» (Jira банка, 25.10.2026), а бэклог Scrum ставит оценку и без экрана. Текущее значение — +`GET /rest/agile/1.0/issue/{key}/estimation?boardId=` (`{fieldId, value}`); отличается — `PUT` туда же с +`{"value": …}` (`null` — очистить); сравнение — как у v15 (`3` равно `3.0`, `null` — только `null`). `field_ids.story_points` +для записи с доской не нужен. `GET /issue` и `PUT /issue` агент делает только для остальных полей команды; в `changed` — +`story_points`, если оценка записана. Если в той же команде записан и `PUT /issue`, перед записью оценки — зазор типа +`ensure_fields` (политика темпа). Если `field_ids.story_points` задан и `fieldId` оценки доски с ним не совпадает — +`failed` «у доски оценка — поле , а не story points ()», без записи. Команда без +`board_id` — оценка через `PUT /issue`, как v15. Ошибка Jira на +`estimation` — `failed` с текстом Jira; остальные поля к этому моменту уже записаны (повтор их не меняет). + +#### Scenario: Доска с закрытыми спринтами +- **WHEN** `refresh {jql: …, boards: [{id: 4216, type: "scrum"}]}`, у доски закрытые спринты 1–2, активный 3, будущий 4 +- **THEN** первая пачка состояния несёт `boards` с доской 4216, четырьмя спринтами и колонками; у задачи спринта 2, + перенесённой в 3, — `sprint_ids: [2, 3]` + +#### Scenario: Нет поля оценки +- **WHEN** в Jira нет поля «Story Points», `field_ids.story_points` не задан +- **THEN** у задач нет ключа `story_points`; `ensure_fields {key: X, story_points: 3}` — `failed` «поле оценки не найдено + в Jira: задайте field_ids.story_points» + +#### Scenario: Оценка не на экране редактирования +- **WHEN** агент v16, `ensure_fields {key: AURORA-1704, story_points: 2, board_id: 4216}`, оценка в Jira — `1.0` +- **THEN** `GET /rest/agile/1.0/issue/AURORA-1704/estimation?boardId=4216`, затем `PUT` туда же `{"value": 2}`; `PUT /issue` + нет; отчёт `done` с `changed: ["story_points"]`, `jira_calls = 2` + +#### Scenario: Доска оценивает другим полем +- **WHEN** `field_ids.story_points = customfield_15852`, `GET …/estimation?boardId=5120` отдаёт `fieldId: timeoriginalestimate` +- **THEN** `failed` «у доски 5120 оценка — поле timeoriginalestimate, а не story points (customfield_15852)», `PUT` нет + +#### Scenario: Оценка уже стоит +- **WHEN** та же команда, оценка в Jira — `2.0` +- **THEN** только `GET …/estimation`; отчёт `done` с `changed: []`, `jira_calls = 1` diff --git a/openspec/changes/sp-estimation-board/specs/commands/spec.md b/openspec/changes/sp-estimation-board/specs/commands/spec.md new file mode 100644 index 0000000..155704c --- /dev/null +++ b/openspec/changes/sp-estimation-board/specs/commands/spec.md @@ -0,0 +1,118 @@ +## MODIFIED Requirements + +### Requirement: Команда — желаемый результат на одной задаче +Каждая команда SHALL описывать желаемое состояние одной задачи Jira и выполняться агентом как «прочитать → сравнить → +записать только разницу». Повторное выполнение любой команды с тем же результатом в Jira SHALL завершаться `done` +без записи. `fetch_attachment` в Jira не пишет: она читает файл вложения и передаёт его хабу; повтор безопасен. Типы команд и их контракт: + +| Тип | Параметры | Что читает | Что пишет | +|---|---|---|---| +| `ensure_issue` | `temp_key` (`new:`), `issue_type` (`task`/`epic`/`bug`), `summary`, `description`, `priority`, `duedate`, `planned_start`, `planned_end`, `labels`, `epic_key`, `assignee` | поиск `project = AND reporter = currentUser() AND created >= -7d`, точное сравнение `summary` | при отсутствии — `POST /issue` с типом из `issue_types.` (для эпика — с `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=` | `PUT /issue/{key}` только с отличающимися полями, кроме оценки; эпик — по `epic_link.mode`; оценка отличается — `PUT /rest/agile/1.0/issue/{key}/estimation?boardId=` с `{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" | "kanban" | null}` для досок, чей тип агент запрашивал | +| `fetch_attachment` | `key`, `attachment_id` (id вложения Jira, строка из цифр) | `GET /issue/{key}?fields=attachment` (имя и размер вложения) | ничего в Jira; содержимое — `GET /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: }` (`project = + AND labels in ("<метка>")`); иначе — как раньше, по ключам команд. Подтверждения по состоянию зеркала у +`ensure_sprint_meta`, `ensure_sprint_state`, `ensure_rank` нет. + +`ensure_sprint_meta` SHALL искать спринт по имени среди всех спринтов доски: закрытый — `failed` «спринт закрыт»; +несколько с этим именем — `failed` «несколько спринтов с именем …: »; `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: , 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-доске» diff --git a/openspec/changes/sp-estimation-board/specs/mcp/spec.md b/openspec/changes/sp-estimation-board/specs/mcp/spec.md new file mode 100644 index 0000000..6272d6a --- /dev/null +++ b/openspec/changes/sp-estimation-board/specs/mcp/spec.md @@ -0,0 +1,22 @@ +## MODIFIED Requirements + +### Requirement: Спринты и бэклог в MCP +MCP `sdlc-jira` SHALL давать инструменты чтения по `GET /agile` проекта сессии (сессия без проекта — тот же отказ, что у +прочих инструментов с проектом): +- `sprints` — таблица спринтов доски: имя, состояние, даты, задач (выполнено/всего), SP (выполнено/всего), без оценки; + закрытые — 10 последних и строка «ещё N закрытых», если их больше; без доски — «у проекта нет доски»; +- `sprint` (`name`, по умолчанию — активный) — шапка спринта (имя, состояние, даты, цель, суммы) и задачи по колонкам + доски: ключ, тема (до 80 символов), исполнитель, SP; +- `backlog` (`limit`, по умолчанию 50) — бэклог по рангу: позиция, ключ, тема, статус, исполнитель, SP; в конце «ещё N», + если задач больше `limit`. +Таблица `issues` SHALL получить колонку «SP». Описание `batch_create` SHALL перечислять `ensure_sprint_meta`, +`ensure_sprint_state`, `ensure_rank` и `story_points` в `ensure_fields` с параметрами; хвостовой `refresh` и `board_id` +(у `ensure_sprint`, команд спринта и `ensure_fields` с `story_points`) хаб дописывает сам (см. `commands`). + +#### Scenario: Спринты проекта +- **WHEN** сессия привязана к `AWPCA`, вызван `sprints` +- **THEN** строки: активный `AWPCA_Sprint_3_Corrections`, будущие, затем закрытые; у каждой — задачи и SP «выполнено/всего» + +#### Scenario: Бэклог с лимитом +- **WHEN** `backlog {limit: 2}`, в бэклоге 188 задач +- **THEN** две строки по рангу и «ещё 186» diff --git a/src/sdlc_hub/modules/jira/hooks.py b/src/sdlc_hub/modules/jira/hooks.py index 6fd0af1..17e2a9e 100644 --- a/src/sdlc_hub/modules/jira/hooks.py +++ b/src/sdlc_hub/modules/jira/hooks.py @@ -52,33 +52,41 @@ def check_setup(conn, cmds, files_dir) -> None: def _assign_boards(conn, cmds) -> None: - """Доска проекта задачи в params ensure_sprint (design #7): метки временного ключа — из его ensure_issue в пакете, - иначе из зеркала; 422 — задача вне проектов, разные доски, нет доски, канбан, тип ещё не известен.""" + """Доска проекта задачи в params ensure_sprint (design #7) и ensure_fields со story_points (оценка через доску, + sp-estimation-board): метки временного ключа — из его ensure_issue в пакете, иначе из зеркала; 422 — задача вне + проектов, разные доски, нет доски, канбан, тип ещё не известен.""" temp_labels = {c["params"]["temp_key"]: c["params"].get("labels") or [] for c in cmds if c["type"] == "ensure_issue"} found = jira_projects.light_list(conn) for i, c in enumerate(cmds, 1): - if c["type"] != "ensure_sprint": + points = c["type"] == "ensure_fields" and "story_points" in c["params"] + if c["type"] != "ensure_sprint" and not points: continue + goal, kanban = ("оценки", KANBAN_POINTS) if points else ("спринта", KANBAN_SPRINT) key = c["params"]["key"] labels = temp_labels[key] if key in temp_labels else (mirror.labels_of(conn, key) or []) own = [p for p in found if p["label"] in labels] if not own: - raise CommandError(f"команда {i}: {key} не входит ни в один проект — доску для спринта не определить") + raise CommandError(f"команда {i}: {key} не входит ни в один проект — доску для {goal} не определить") if len({p["board_id"] for p in own}) > 1: raise CommandError(f"команда {i}: {key} в проектах {', '.join(p['label'] for p in own)} с разными досками") p = own[0] - err = _board_error(p["label"], p["board_id"], p["board_type"]) + err = _board_error(p["label"], p["board_id"], p["board_type"], kanban) if err: raise CommandError(f"команда {i}: {err}") c["params"] = {**c["params"], "board_id": p["board_id"]} -def _board_error(label, board_id, board_type) -> str | None: - """Общие тексты 422 для ensure_sprint и команд спринта: доски нет, канбан, тип ещё не известен; None — доска годится.""" +KANBAN_SPRINT = "спринтов нет" +KANBAN_POINTS = "оценка — только на scrum-доске" + + +def _board_error(label, board_id, board_type, kanban=KANBAN_SPRINT) -> str | None: + """Общие тексты 422 для ensure_sprint, команд спринта и оценки: доски нет, канбан (хвост текста — kanban), тип ещё + не известен; None — доска годится.""" if board_id is None: return f"у проекта {label} нет доски Jira" if board_type == "kanban": - return f"у проекта {label} канбан-доска, спринтов нет" + return f"у проекта {label} канбан-доска, {kanban}" if board_type is None: return f"тип доски проекта {label} ещё не известен — дождитесь обновления из Jira" return None @@ -282,8 +290,8 @@ def check_confirmed(conn, cmd) -> tuple[bool, dict | None]: def retry_params(cmd) -> dict: - """Повтор ensure_sprint и команд спринта берёт доску заново (она в params ставится при постановке).""" - drop = cmd["type"] in ("ensure_sprint", *commands.SPRINT_TYPES) + """Повтор ensure_sprint, команд спринта и ensure_fields берёт доску заново (она в params ставится при постановке).""" + drop = cmd["type"] in ("ensure_sprint", "ensure_fields", *commands.SPRINT_TYPES) return {k: v for k, v in cmd["params"].items() if not (drop and k == "board_id")} diff --git a/src/sdlc_hub/modules/jira/mcp.py b/src/sdlc_hub/modules/jira/mcp.py index b4fd06e..5a97874 100644 --- a/src/sdlc_hub/modules/jira/mcp.py +++ b/src/sdlc_hub/modules/jira/mcp.py @@ -497,7 +497,7 @@ def _doc(text: str): @_doc("""Поставить пакет команд. Каждая команда — {"type": <тип>, "params": {...}}. Хвостовой refresh хаб добавит сам. Параметры по типам (необязательные — в [ ]): """ + describe() + """ -Проект: метку проекта сессии хаб дописывает в ensure_issue сам, метки других проектов запрещены, а команды над задачами вне проекта отклоняются; board_id для ensure_sprint, ensure_sprint_meta и ensure_sprint_state выставляет хаб; ensure_sprint_meta и ensure_sprint_state — только в сессии с проектом, хвост такого пакета — refresh по JQL проекта; оценка — story_points в ensure_fields. +Проект: метку проекта сессии хаб дописывает в ensure_issue сам, метки других проектов запрещены, а команды над задачами вне проекта отклоняются; board_id для ensure_sprint, ensure_sprint_meta, ensure_sprint_state и ensure_fields со story_points выставляет хаб; ensure_sprint_meta и ensure_sprint_state — только в сессии с проектом, хвост такого пакета — refresh по JQL проекта; оценка — story_points в ensure_fields. ensure_attachment из чата: вместо file передайте path — абсолютный путь к локальному файлу (до 10 МБ); MCP загрузит файл в хаб и подставит file, filename по умолчанию — имя файла.""") def batch_create(title: str, commands: list[dict]) -> str: diff --git a/tests/fixtures/baseline/mcp_tools.expected.json b/tests/fixtures/baseline/mcp_tools.expected.json index d70e36d..ea8229d 100644 --- a/tests/fixtures/baseline/mcp_tools.expected.json +++ b/tests/fixtures/baseline/mcp_tools.expected.json @@ -228,7 +228,7 @@ "name": "notes_set" }, { - "description": "Поставить пакет команд. Каждая команда — {\"type\": <тип>, \"params\": {...}}. Хвостовой refresh хаб добавит сам.\nПараметры по типам (необязательные — в [ ]):\n- ensure_issue: temp_key (new:), issue_type (task|epic|bug), summary; [description, priority, duedate (YYYY-MM-DD), planned_start (YYYY-MM-DD), planned_end (YYYY-MM-DD), labels (список), epic_key (ключ или new:), assignee (логин Jira)]\n- ensure_fields: key; [summary, description, priority, duedate (YYYY-MM-DD), planned_start (YYYY-MM-DD), planned_end (YYYY-MM-DD), epic_key (ключ или new:), assignee (логин Jira), labels_add (список), labels_remove (список), story_points (число ≥ 0 или null)] — хотя бы одно поле\n- ensure_comment: key, marker (метка в начале комментария), body\n- ensure_status: key, target (статус или список-путь)\n- ensure_link: source, target, link_type (логическое имя из link_types); [replace (true|false)]\n- ensure_sprint: key, sprint (имя спринта или backlog)\n- ensure_attachment: key, file (sha256 из POST /files; в MCP — path к локальному файлу), filename (имя вложения)\n- fetch_attachment: key, attachment_id (id вложения из issue (цифры))\n- refresh: —; [keys (список), jql] — keys или jql\n- ensure_sprint_meta: sprint (имя спринта); [rename_to (новое имя), start_date (YYYY-MM-DD), end_date (YYYY-MM-DD), goal (цель спринта)]\n- ensure_sprint_state: sprint, state (active|closed); [move_open_to (имя спринта или backlog; обязателен при closed)]\n- ensure_rank: key; [before (ключ задачи, выше которой), after (ключ задачи, ниже которой)] — ровно одно из before, after\nПроект: метку проекта сессии хаб дописывает в ensure_issue сам, метки других проектов запрещены, а команды над задачами вне проекта отклоняются; board_id для ensure_sprint, ensure_sprint_meta и ensure_sprint_state выставляет хаб; ensure_sprint_meta и ensure_sprint_state — только в сессии с проектом, хвост такого пакета — refresh по JQL проекта; оценка — story_points в ensure_fields.\nensure_attachment из чата: вместо file передайте path — абсолютный путь к локальному файлу (до 10 МБ); MCP загрузит\nфайл в хаб и подставит file, filename по умолчанию — имя файла.", + "description": "Поставить пакет команд. Каждая команда — {\"type\": <тип>, \"params\": {...}}. Хвостовой refresh хаб добавит сам.\nПараметры по типам (необязательные — в [ ]):\n- ensure_issue: temp_key (new:), issue_type (task|epic|bug), summary; [description, priority, duedate (YYYY-MM-DD), planned_start (YYYY-MM-DD), planned_end (YYYY-MM-DD), labels (список), epic_key (ключ или new:), assignee (логин Jira)]\n- ensure_fields: key; [summary, description, priority, duedate (YYYY-MM-DD), planned_start (YYYY-MM-DD), planned_end (YYYY-MM-DD), epic_key (ключ или new:), assignee (логин Jira), labels_add (список), labels_remove (список), story_points (число ≥ 0 или null)] — хотя бы одно поле\n- ensure_comment: key, marker (метка в начале комментария), body\n- ensure_status: key, target (статус или список-путь)\n- ensure_link: source, target, link_type (логическое имя из link_types); [replace (true|false)]\n- ensure_sprint: key, sprint (имя спринта или backlog)\n- ensure_attachment: key, file (sha256 из POST /files; в MCP — path к локальному файлу), filename (имя вложения)\n- fetch_attachment: key, attachment_id (id вложения из issue (цифры))\n- refresh: —; [keys (список), jql] — keys или jql\n- ensure_sprint_meta: sprint (имя спринта); [rename_to (новое имя), start_date (YYYY-MM-DD), end_date (YYYY-MM-DD), goal (цель спринта)]\n- ensure_sprint_state: sprint, state (active|closed); [move_open_to (имя спринта или backlog; обязателен при closed)]\n- ensure_rank: key; [before (ключ задачи, выше которой), after (ключ задачи, ниже которой)] — ровно одно из before, after\nПроект: метку проекта сессии хаб дописывает в ensure_issue сам, метки других проектов запрещены, а команды над задачами вне проекта отклоняются; board_id для ensure_sprint, ensure_sprint_meta, ensure_sprint_state и ensure_fields со story_points выставляет хаб; ensure_sprint_meta и ensure_sprint_state — только в сессии с проектом, хвост такого пакета — refresh по JQL проекта; оценка — story_points в ensure_fields.\nensure_attachment из чата: вместо file передайте path — абсолютный путь к локальному файлу (до 10 МБ); MCP загрузит\nфайл в хаб и подставит file, filename по умолчанию — имя файла.", "inputSchema": { "properties": { "commands": { diff --git a/tests/test_mcp.py b/tests/test_mcp.py index 59781ad..c50c017 100644 --- a/tests/test_mcp.py +++ b/tests/test_mcp.py @@ -221,7 +221,7 @@ def test_fmt_batch_warns_about_claimed(): def test_batch_create_mentions_labels(): doc = jmcp.batch_create.__doc__ or "" assert "метку проекта сессии хаб дописывает в ensure_issue сам" in doc - assert "метки других проектов запрещены" in doc and "board_id для ensure_sprint, ensure_sprint_meta и ensure_sprint_state выставляет хаб" in doc + assert "метки других проектов запрещены" in doc and "board_id для ensure_sprint, ensure_sprint_meta, ensure_sprint_state и ensure_fields со story_points выставляет хаб" in doc assert "AWPCA" not in doc diff --git a/tests/test_queue.py b/tests/test_queue.py index 661d559..d6544f0 100644 --- a/tests/test_queue.py +++ b/tests/test_queue.py @@ -895,6 +895,82 @@ def test_retry_premigration_sprint_without_board(conn, add_project): # Review assert queue.batch_view(conn, r["id"])["commands"][0]["params"] == {"key": "A-1", "sprint": "Спринт 2", "board_id": 4216} + +def fields_sp(key, sp=1): + return {"type": "ensure_fields", "params": {"key": key, "story_points": sp}} + + +def test_story_points_gets_project_board(conn, add_project): # сценарий «Оценка задачи проекта» + add_project(conn) + mirror.apply_state(conn, [{"key": "AURORA-1692", "labels": ["AWPCA"]}], []) + b = queue.create_batch(conn, "x", "claude", [fields_sp("AURORA-1692")]) + assert queue.batch_view(conn, b["id"])["commands"][0]["params"] == {"key": "AURORA-1692", "story_points": 1, "board_id": 4216} + + +@pytest.mark.parametrize("setup, key, msg", [ + (dict(board_id=5120, board_type="kanban"), "AURORA-1", + "команда 1: у проекта STOCKS_DRRK канбан-доска, оценка — только на scrum-доске"), + (dict(board_id=5120, board_type=None), "AURORA-1", "тип доски проекта STOCKS_DRRK ещё не известен"), + (dict(board_id=None, board_type=None), "AURORA-1", "у проекта STOCKS_DRRK нет доски Jira"), + (dict(board_id=5120, board_type="scrum"), "AURORA-77", + "команда 1: AURORA-77 не входит ни в один проект — доску для оценки не определить"), + (dict(board_id=5120, board_type="scrum"), "AURORA-12", "с разными досками"), +]) +def test_story_points_board_rejected(conn, add_project, setup, key, msg): # сценарии «Оценка вне проектов», «на канбане» + add_project(conn) + add_project(conn, "STOCKS_DRRK", **setup) + mirror.apply_state(conn, [{"key": "AURORA-1", "labels": ["STOCKS_DRRK"]}, {"key": "AURORA-77", "labels": ["kind-dev"]}, + {"key": "AURORA-12", "labels": ["AWPCA", "STOCKS_DRRK"]}], []) + with pytest.raises(CommandError, match=msg): + queue.create_batch(conn, "x", "claude", [fields_sp(key)]) + assert conn.execute("SELECT COUNT(*) AS n FROM batch").fetchone()["n"] == 0 + + +def test_fields_without_points_no_board(conn, add_project): # сценарий «Поля без оценки — без доски» + add_project(conn) + mirror.apply_state(conn, [{"key": "AURORA-77", "labels": ["kind-dev"]}], []) + b = queue.create_batch(conn, "x", "claude", [{"type": "ensure_fields", "params": {"key": "AURORA-77", "duedate": "2026-10-30"}}]) + assert "board_id" not in queue.batch_view(conn, b["id"])["commands"][0]["params"] + + +def test_story_points_for_new_issue_uses_batch_labels(conn, add_project): + add_project(conn) + b = queue.create_batch(conn, "x", "claude", [ + {"type": "ensure_issue", "params": {"temp_key": "new:1", "issue_type": "task", "summary": "Н"}}, fields_sp("new:1")], + project="AWPCA") + assert queue.batch_view(conn, b["id"])["commands"][1]["params"]["board_id"] == 4216 + + +def test_fields_and_sprint_same_issue(conn, add_project): + add_project(conn) + mirror.apply_state(conn, [{"key": "A-1", "labels": ["AWPCA"]}], []) + b = queue.create_batch(conn, "x", "claude", [fields_sp("A-1"), sprint("A-1")]) + assert [c["params"]["board_id"] for c in queue.batch_view(conn, b["id"])["commands"][:2]] == [4216, 4216] + + +def test_client_board_id_in_fields_rejected(conn, add_project): + add_project(conn) + with pytest.raises(CommandError, match="лишние параметры board_id"): + queue.create_batch(conn, "x", "claude", [{"type": "ensure_fields", "params": {"key": "A-1", "story_points": 1, "board_id": 1}}]) + + +def test_retry_story_points_recomputes_board(conn, add_project): + add_project(conn) + mirror.apply_state(conn, [{"key": "AURORA-1692", "labels": ["AWPCA"]}], []) + b = queue.create_batch(conn, "x", "claude", [fields_sp("AURORA-1692")]) + cid = queue.batch_view(conn, b["id"])["commands"][0]["id"] + conn.execute("UPDATE jira_project SET board_id = 5120, board_type = 'scrum'") + r = queue.retry_command(conn, cid, "claude") + assert queue.batch_view(conn, r["id"])["commands"][0]["params"]["board_id"] == 5120 + + +def test_retry_fields_without_points_keeps_params(conn, add_project): + add_project(conn) + p = {"key": "AURORA-77", "duedate": "2026-10-30"} + b = queue.create_batch(conn, "x", "claude", [{"type": "ensure_fields", "params": dict(p)}]) + r = queue.retry_command(conn, queue.batch_view(conn, b["id"])["commands"][0]["id"], "claude") + assert queue.batch_view(conn, r["id"])["commands"][0]["params"] == p + def meta(name="AWPCA_Sprint_4"): return {"type": "ensure_sprint_meta", "params": {"sprint": name, "start_date": "2026-10-28", "end_date": "2026-11-11"}} def close(name="AWPCA_Sprint_3_Corrections", to="AWPCA_Sprint_4"):