diff --git a/openspec/changes/projects/.openspec.yaml b/openspec/changes/archive/2026-10-19-projects/.openspec.yaml similarity index 100% rename from openspec/changes/projects/.openspec.yaml rename to openspec/changes/archive/2026-10-19-projects/.openspec.yaml diff --git a/openspec/changes/projects/design.md b/openspec/changes/archive/2026-10-19-projects/design.md similarity index 100% rename from openspec/changes/projects/design.md rename to openspec/changes/archive/2026-10-19-projects/design.md diff --git a/openspec/changes/projects/proposal.md b/openspec/changes/archive/2026-10-19-projects/proposal.md similarity index 100% rename from openspec/changes/projects/proposal.md rename to openspec/changes/archive/2026-10-19-projects/proposal.md diff --git a/openspec/changes/projects/specs/agent/spec.md b/openspec/changes/archive/2026-10-19-projects/specs/agent/spec.md similarity index 100% rename from openspec/changes/projects/specs/agent/spec.md rename to openspec/changes/archive/2026-10-19-projects/specs/agent/spec.md diff --git a/openspec/changes/projects/specs/commands/spec.md b/openspec/changes/archive/2026-10-19-projects/specs/commands/spec.md similarity index 100% rename from openspec/changes/projects/specs/commands/spec.md rename to openspec/changes/archive/2026-10-19-projects/specs/commands/spec.md diff --git a/openspec/changes/projects/specs/data-model/spec.md b/openspec/changes/archive/2026-10-19-projects/specs/data-model/spec.md similarity index 100% rename from openspec/changes/projects/specs/data-model/spec.md rename to openspec/changes/archive/2026-10-19-projects/specs/data-model/spec.md diff --git a/openspec/changes/projects/specs/hub-api/spec.md b/openspec/changes/archive/2026-10-19-projects/specs/hub-api/spec.md similarity index 100% rename from openspec/changes/projects/specs/hub-api/spec.md rename to openspec/changes/archive/2026-10-19-projects/specs/hub-api/spec.md diff --git a/openspec/changes/projects/specs/mcp/spec.md b/openspec/changes/archive/2026-10-19-projects/specs/mcp/spec.md similarity index 100% rename from openspec/changes/projects/specs/mcp/spec.md rename to openspec/changes/archive/2026-10-19-projects/specs/mcp/spec.md diff --git a/openspec/changes/projects/specs/migration/spec.md b/openspec/changes/archive/2026-10-19-projects/specs/migration/spec.md similarity index 100% rename from openspec/changes/projects/specs/migration/spec.md rename to openspec/changes/archive/2026-10-19-projects/specs/migration/spec.md diff --git a/openspec/changes/projects/specs/web-app/spec.md b/openspec/changes/archive/2026-10-19-projects/specs/web-app/spec.md similarity index 100% rename from openspec/changes/projects/specs/web-app/spec.md rename to openspec/changes/archive/2026-10-19-projects/specs/web-app/spec.md diff --git a/openspec/specs/agent/spec.md b/openspec/specs/agent/spec.md index dbc1d2e..f93d996 100644 --- a/openspec/specs/agent/spec.md +++ b/openspec/specs/agent/spec.md @@ -11,9 +11,9 @@ или `bearer` с `token`), `verify_ssl`, `ca_bundle`, `timeout`, `poll_seconds`, необязательный `auto_update` (по умолчанию `true`). `hub_url` вида `http://…` или `https://…` SHALL означать HTTP-транспорт, вида `folder:<абсолютный путь>` — транспорт через общую папку. Всё остальное (id полей, типы, карта переходов, типы связей, -доска, темп) агент SHALL получать от хаба при старте и при изменении `settings_version`. Учётные данные Jira MUST NOT -отправляться в хаб и MUST NOT попадать в журнал, логи и общую папку. Команды `agent.py`: `run` (по умолчанию), `check`, -`selftest`, `prune`. +темп) агент SHALL получать от хаба при старте и при изменении `settings_version`; доски Jira — в параметрах команд +(требование «Доски проектов»). Учётные данные Jira MUST NOT отправляться в хаб и MUST NOT попадать в журнал, логи и общую +папку. Команды `agent.py`: `run` (по умолчанию), `check`, `selftest`, `prune`. #### Scenario: Проверка без записи - **WHEN** `python3 agent.py check` @@ -392,3 +392,31 @@ README агента SHALL описывать: первую установку ( #### Scenario: Старый агент - **WHEN** агент v10 получил `ensure_attachment` - **THEN** `failed` «неизвестный тип команды», агент работает дальше (поведение v10, без изменений) + +### Requirement: Доски проектов +Агент (с v12) SHALL брать доски Jira из параметров команд, а не из настроек: `ensure_sprint` — из `board_id` команды, +`refresh` — из `boards` (`[{id, type}]`). В `refresh` для каждой доски с `type = null` агент SHALL запросить +`GET /rest/agile/1.0/board/{id}` и вернуть в результате команды `boards: {"": "scrum" | "kanban" | null}` (`null` — +доска не найдена, нет доступа или тип не `scrum`/`kanban`; причина — в лог агента; `refresh` продолжается). Имена +спринтов задач агент SHALL собирать со всех досок типа `scrum` (известного или только что определённого), доски типа +`kanban` и `null` пропускать; задача в спринтах двух досок получает последнее найденное имя (как в v11). Если доска, которую хаб передал как `scrum`, не отдала спринты, агент SHALL перепроверить её тип: стала `kanban` — доска пропускается, в результате `boards: {"": "kanban"}` (хаб записывает тип и при прежнем `scrum`); иначе `refresh` — `failed` с текстом «доска : …» (агент v12, R-36). Команда без `board_id`/`boards` +(хаб до проектов) SHALL выполняться по `settings.board_id`, как в v11. `ensure_sprint` с `board_id` канбан-доски +агент не получает (хаб отказывает при постановке); если Jira всё же ответила, что у доски нет спринтов, — `failed` с +текстом Jira. + +#### Scenario: Две доски, одна новая +- **WHEN** `refresh {jql: …, boards: [{id: 4216, type: "scrum"}, {id: 5120, type: null}]}`, доска 5120 в Jira — kanban +- **THEN** агент запрашивает `GET /board/5120` один раз, спринты читает только с доски 4216; результат содержит + `boards: {"5120": "kanban"}` + +#### Scenario: Доска не найдена +- **WHEN** `refresh` с `boards: [{id: 9999, type: null}]`, Jira на `GET /board/9999` ответила 404 +- **THEN** результат `refresh` — `done` с `boards: {"9999": null}`; в логе агента — доска 9999 и код ответа + +#### Scenario: Старый хаб +- **WHEN** агент v12, хаб без проектов, `refresh {jql: …}` без `boards`, `settings.board_id = 4216` +- **THEN** спринты читаются с доски 4216, результат без `boards` + +#### Scenario: Спринт по доске из команды +- **WHEN** `ensure_sprint {key: AURORA-1538, sprint: "Спринт 2", board_id: 4216}` +- **THEN** агент ищет «Спринт 2» среди активных и будущих спринтов доски 4216 и ставит задачу в него diff --git a/openspec/specs/commands/spec.md b/openspec/specs/commands/spec.md index 708655b..b748a58 100644 --- a/openspec/specs/commands/spec.md +++ b/openspec/specs/commands/spec.md @@ -17,9 +17,17 @@ | `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`) | `GET /issue/{key}` (поле спринта), список спринтов доски из `board_id` (активные и будущие) | `POST /rest/agile/1.0/sprint/{id}/issue` или `POST /rest/agile/1.0/backlog/issue` | +| `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_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` | `search` с полями зеркала, по 100 на страницу; плюс дочитывание соседей по `issuelinks` | ничего в Jira; состояние уходит в хаб | +| `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}` для досок, чей тип агент запрашивал | + +Доску для `ensure_sprint` хаб SHALL определять при постановке: проект задачи — проекты, чьи метки есть у задачи в +зеркале (для временного ключа — в `labels` её `ensure_issue` в пакете, после дописывания метки проекта); `board_id` — +доска этого проекта. 422 при постановке: задача ни в одном проекте; в нескольких проектах с разными досками; у проекта +нет доски; тип доски `kanban` («у проекта P канбан-доска, спринтов нет»); тип доски ещё не известен («тип доски проекта P +ещё не известен — дождитесь обновления из Jira»). `boards` у `refresh` хаб SHALL дописывать при каждой выдаче (в базе +не хранится): все доски проектов без повторов. По результату `refresh` хаб SHALL записать присланные типы `scrum` и +`kanban` в проекты с этой доской; `null` тип не меняет. Семантика `ensure_link`: `source` блокирует/ссылается на `target`; агент в `POST /issueLink` SHALL ставить `source` в `inwardIssue`, `target` в `outwardIssue` (проверено на Jira банка 11.10.2026). @@ -67,6 +75,18 @@ - **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` + ### Requirement: Временные ключи и ссылки внутри пакета Параметр-ключ любой команды MAY быть временным `new:`. Хаб SHALL выдавать агенту такую команду только после `done` команды `ensure_issue` с тем же `temp_key` в том же пакете, подставив настоящий ключ. Если `ensure_issue` завершилась @@ -198,3 +218,35 @@ SHALL перевести её в `confirmed_by_state`: `ensure_fields` — вс - **WHEN** пакет `[ensure_issue {temp_key: new:1, issue_type: bug, …}, ensure_sprint {key: new:1, sprint: "Спринт 2"}, ensure_attachment {key: new:1, file: , filename: "ОСВ 60.xlsx"}]`, файл загружен - **THEN** пакет создан; `ensure_attachment` выдаётся после завершения `ensure_issue` с подставленным ключом и `size` файла + +### Requirement: Пакет проекта +Пакет, поставленный с `project = P` (см. `hub-api`), SHALL проходить при постановке, до записи, проверки проекта; +любая неудача — 422 с ключом задачи и причиной, пакет не создаётся: +- `P` нет среди проектов — 422; +- `ensure_issue`: если в `labels` нет P, хаб SHALL дописать P в конец `labels` (в базе параметры хранятся уже с ней; + без `labels` — `labels = [P]`); метка, совпадающая с P без учёта регистра, но не точно, SHALL заменяться на P; метка + другого проекта в `labels` — 422; +- `ensure_fields`: P в `labels_remove` или метка другого проекта в `labels_add` — 422; +- `ensure_fields`, `ensure_comment`, `ensure_status`, `ensure_sprint`, `ensure_attachment`: `key` SHALL быть временным + ключом `ensure_issue` этого пакета или задачей зеркала с меткой P, иначе 422 « не в проекте P»; +- `ensure_link`: хотя бы один из `source`, `target` — временный ключ этого пакета или задача зеркала с меткой P; +- `refresh` — без проверок проекта. +«Метка другого проекта» и «P в `labels_remove`» SHALL сравниваться без учёта регистра; принадлежность задачи проекту +(метка P у задачи зеркала) — точным совпадением, как в разрезе проекта. Пакет без `project` эти проверки не проходит, +метки хаб в нём не дописывает. + +#### Scenario: Метка проекта дописана +- **WHEN** пакет с `project = STOCKS_DRRK`, `ensure_issue {temp_key: new:1, summary: "Сверка остатков", labels: ["kind-dev"]}` +- **THEN** пакет создан; в параметрах команды `labels = ["kind-dev", "STOCKS_DRRK"]` + +#### Scenario: Чужая метка проекта +- **WHEN** пакет с `project = STOCKS_DRRK`, `ensure_issue {…, labels: ["awpca"]}`, есть проект `AWPCA` +- **THEN** 422 «метка другого проекта: awpca», пакет не создан + +#### Scenario: Задача другого проекта +- **WHEN** пакет с `project = STOCKS_DRRK`, `ensure_fields {key: AURORA-1538, duedate: …}`, у AURORA-1538 метка только `AWPCA` +- **THEN** 422 «AURORA-1538 не в проекте STOCKS_DRRK» + +#### Scenario: Связь с соседом +- **WHEN** пакет с `project = STOCKS_DRRK`, `ensure_link {source: AURORA-1 (метка STOCKS_DRRK), target: AURORA-1538 (метка AWPCA), …}` +- **THEN** пакет создан diff --git a/openspec/specs/data-model/spec.md b/openspec/specs/data-model/spec.md index a81aace..31aa820 100644 --- a/openspec/specs/data-model/spec.md +++ b/openspec/specs/data-model/spec.md @@ -7,15 +7,24 @@ ### Requirement: Схема базы хаба База хаба SHALL быть одним файлом SQLite (WAL) с таблицами `issue`, `issue_link`, `issue_comment`, `issue_note`, `batch`, -`command`, `sync_run`, `agent_status`, `setting`. Схема SHALL создаваться и мигрироваться самим хабом при старте по номеру -версии в `setting.schema_version` (текущая — 3). Миграция существующей базы SHALL выполняться в одной транзакции с -блокировкой записи; в той же транзакции, после повторной проверки версии, хаб SHALL скопировать файл базы в +`command`, `sync_run`, `agent_status`, `setting`, `project`. Схема SHALL создаваться и мигрироваться самим хабом при +старте по номеру версии в `setting.schema_version` (текущая — 4). Миграция существующей базы SHALL выполняться в одной +транзакции с блокировкой записи; в той же транзакции, после повторной проверки версии, хаб SHALL скопировать файл базы в `backups/<дата-время>-schema-v<старая>.sqlite` рядом с базой — одна копия на миграцию, сколько бы соединений ни открывалось одновременно. Каталог данных хаба и `backups/` SHALL иметь права `0700` (в базе токены): хаб выставляет их при создании и при каждом старте; если выставить не удалось — предупреждение в лог, работа продолжается. Денежных и дробных полей нет; даты — ISO-8601 строки в UTC, кроме `duedate` и плановых дат (даты без времени) и времени комментариев (строки Jira как есть). +Миграция v3 → v4 SHALL: создать `project` и колонку `batch.project` (у существующих пакетов `NULL`); разобрать прежнюю +настройку `base_jql` вида `… labels = ` или `… labels in (, …)` и завести проекты с этими метками (название проекта +`AWPCA` — «АРМ КА», остальных — метка; `board_type = NULL`); отдать прежний `board_id` проекту, если проект один, иначе +никому, с предупреждением в лог; у строк `sync_run` с `query`, равной прежней `base_jql`, и у команд `refresh` в +`queued`/`claimed` с `params.jql`, равной прежней `base_jql`, записать базовую JQL, вычисленную из проектов. Ключи +`base_jql` и `board_id` в `setting` миграция MUST NOT удалять: базу может открыть процесс новой версии раньше, чем +перезапущен хаб прежней версии, который их читает. Не разобранная `base_jql` — проектов нет, предупреждение в лог с +прежним значением. + #### Scenario: Первый запуск - **WHEN** хаб стартует с несуществующим файлом базы - **THEN** файл создаётся, таблицы и индексы на месте, `setting.schema_version` = текущая; каталог данных с правами `0700` @@ -23,11 +32,23 @@ #### Scenario: Миграция базы v1 - **WHEN** хаб стартует с базой `schema_version = 1`, в которой есть задачи, заметки и команда в `claimed` - **THEN** в `backups/` копия базы v1; добавлена `command.deliveries` (у `claimed` — 1, у остальных — 0); добавлена пустая - `issue_comment`; данные на месте; `schema_version = 3` + `issue_comment`; данные на месте; `schema_version = 4` #### Scenario: Миграция базы v2 - **WHEN** хаб стартует с базой `schema_version = 2`, в которой есть задачи, связи и заметки -- **THEN** в `backups/` копия базы v2; добавлена пустая `issue_comment`; данные на месте; `schema_version = 3` +- **THEN** в `backups/` копия базы v2; добавлена пустая `issue_comment`; данные на месте; `schema_version = 4` + +#### Scenario: Миграция базы v3 +- **WHEN** хаб стартует с базой `schema_version = 3`, `base_jql = "project = AURORA AND labels = AWPCA"`, `board_id = 4216`, + в `sync_run` полный прогон с этой `query` +- **THEN** в `backups/` копия базы v3; проект `AWPCA` «АРМ КА» с `board_id = 4216`, `board_type = NULL`; ключи `base_jql` + и `board_id` в `setting` на месте; `query` того прогона — `project = AURORA AND labels in ("AWPCA")`; задачи, заметки, + пакеты на месте; `schema_version = 4` + +#### Scenario: refresh в очереди при миграции +- **WHEN** в базе v3 стоит `queued` `refresh {jql: "project = AURORA AND labels = AWPCA"}` +- **THEN** после миграции его `params.jql` — `project = AURORA AND labels in ("AWPCA")`, и по завершении он считается + полным прогоном #### Scenario: Два соединения к базе v1 - **WHEN** к базе `schema_version = 1` одновременно открываются два соединения @@ -88,7 +109,8 @@ MUST NOT менять `issue_note`. При завершении `ensure_issue` - **THEN** у `AURORA-1640` `work_kind = "Аналитика"` и `owner_note = "спросить ИБ"`; строки `new:5` нет ### Requirement: Пакеты и команды -Таблица `batch` SHALL хранить `id`, `title`, `author`, `created_at`, `cancelled_at`, `estimate_seconds`. +Таблица `batch` SHALL хранить `id`, `title`, `author`, `created_at`, `cancelled_at`, `estimate_seconds`, `project` (метка +проекта, если пакет поставлен в разрезе проекта, иначе `NULL`). Таблица `command` SHALL хранить `id`, `batch_id`, `seq`, `type`, `params` (JSON), `status` (`queued | claimed | done | failed | skipped | cancelled | confirmed_by_state`), `claimed_at` (время последней выдачи), `deliveries` (сколько раз команда выдана агенту), `finished_at`, `result` (JSON), `error` (до 1500 символов), `jira_calls`, @@ -127,13 +149,16 @@ SHALL обнулить `issues_count` и `links_count` её `sync_run` (`started - **THEN** у его `sync_run` проставлен `finished_at` ### Requirement: Настройки -Таблица `setting` SHALL хранить парами ключ–JSON: `base_jql`, `field_ids`, `issue_types`, `priority_map`, `link_types`, -`transitions_map`, `pacing`, `board_id`, `tokens` (агентский и клиентский), `settings_version` (увеличивается при любой +Таблица `setting` SHALL хранить парами ключ–JSON: `project_key`, `field_ids`, `issue_types`, `priority_map`, `link_types`, +`transitions_map`, `pacing`, `tokens` (агентский и клиентский), `settings_version` (увеличивается при любой правке), `backup_keep`, `auto_refresh_minutes`, `relay_dir` (абсолютный путь папки реле или `null`, по умолчанию `null`; относительный путь и не-строка при записи — 422; агенту не передаётся), `closed_statuses` (список имён статусов Jira, считающихся закрытыми, по умолчанию `["Готово", "Снято"]`; не-список при записи — 422; агенту не передаётся; на существующей базе появляется со значением -по умолчанию при открытии хаба). Спринты агент читает с доски `board_id`, -отдельной настройки имён спринтов нет. Начальные значения SHALL импортироваться из `admin/jira-sync/config.example.json` +по умолчанию при открытии хаба). Базовая JQL вычисляется из проектов, доска — свойство проекта (требование +«Проекты»); на новой базе ключей `base_jql` и `board_id` нет. Оставшиеся от v3 ключи хаб SHALL NOT использовать, кроме +передачи `board_id` агенту как запасной доски для команд без доски в параметрах (поставленных до миграции); +`GET /settings` SHALL отдавать вычисленную `base_jql` только для чтения, а запись `base_jql` или `board_id` через +`PUT /settings` — 422 с пояснением. Спринты агент читает с досок проектов, отдельной настройки имён спринтов нет. Начальные значения SHALL импортироваться из `admin/jira-sync/config.example.json` командой `jira-hub init`; пароль и логин Jira в настройках MUST NOT присутствовать. `issue_types` — объект `{task, epic, bug}`, у каждого `{name, id}` (`id` может быть `null`); `init` берёт их из ключей конфига `issue_type_<тип>` и `issue_type_<тип>_id`; `bug` попадает в `issue_types`, только если в конфиге есть хотя бы @@ -154,3 +179,52 @@ SHALL обнулить `issues_count` и `links_count` её `sync_run` (`started #### Scenario: Закрытые статусы по умолчанию - **WHEN** хаб открыл базу, в которой нет ключа `closed_statuses` - **THEN** `GET /settings` отдаёт `closed_statuses = ["Готово", "Снято"]`; `claim` агенту этого ключа не передаёт + +#### Scenario: Запись базовой JQL +- **WHEN** `PUT /settings {base_jql: "project = AURORA"}` +- **THEN** 422 «базовая JQL собирается из проектов»; `settings_version` не изменился + +#### Scenario: Создание проекта не трогает версию настроек +- **WHEN** создан проект `STOCKS_DRRK` +- **THEN** `settings_version` прежний; `GET /settings` отдаёт `base_jql` с обеими метками + +### Requirement: Проекты +Таблица `project` SHALL хранить проекты моста — подмножества задач Jira-проекта `project_key`, выделенные меткой: +`label` (PK, метка Jira), `name`, `board_id` (номер доски Jira или `NULL`), `board_type` (`scrum` | `kanban` | `NULL` — +тип ещё не известен), `created_at`. Метка SHALL быть непустой строкой до 100 символов без пробельных и управляющих символов (категория +Unicode `Cc`), запятых, кавычек, скобок и обратной косой черты; две метки, совпадающие без учёта регистра, MUST NOT существовать одновременно. Название SHALL быть +непустым после обрезки пробелов, до 100 символов. `board_id` — целое больше нуля или `NULL`. `board_type` SHALL +записываться только по результату `refresh` от агента (см. `agent`). Метку, название и доску проекта после создания +хаб не меняет (переименования и удаления нет). Базовая JQL SHALL вычисляться из проектов: +`project = AND labels in ("<метка>", …)` — каждая метка в двойных кавычках, метки по created_at, затем +label, через `, `; без проектов базовой JQL +нет (`null`). + +Для проекта P задачи зеркала делятся на *свои* — с меткой P в `labels` (точное совпадение) — и *соседей* — задачи без +метки P, связанные со своей связью `issue_link` в любую сторону, и эпики своих (`epic_key` своей задачи), если они есть в +зеркале. Признак `neighbor` в выдаче в разрезе проекта SHALL считаться относительно P: `false` у своих, `true` у соседей. + +#### Scenario: Базовая JQL из двух проектов +- **WHEN** заведены проекты `AWPCA` (раньше) и `STOCKS_DRRK`, `project_key = AURORA` +- **THEN** базовая JQL — `project = AURORA AND labels in ("AWPCA", "STOCKS_DRRK")` + +#### Scenario: Свои и соседи +- **WHEN** у AURORA-1 метка `STOCKS_DRRK` и эпик AURORA-9 (метка `AWPCA`), связь AURORA-1 → AURORA-2 (метка `AWPCA`), + у AURORA-3 метка `AWPCA` без связей с проектом `STOCKS_DRRK` +- **THEN** в разрезе `STOCKS_DRRK` свои — AURORA-1, соседи — AURORA-2 и AURORA-9; AURORA-3 в разрез не входит + +#### Scenario: Метка с пробелом +- **WHEN** создаётся проект с меткой `STOCKS DRRK` +- **THEN** отказ с указанием поля `label`, проекта нет + +#### Scenario: Метка с обратной косой чертой или управляющим символом +- **WHEN** создаётся проект с меткой `A\B` или `A<перевод строки>B` +- **THEN** отказ с указанием поля `label`, проекта нет + +#### Scenario: Метка — зарезервированное слово JQL +- **WHEN** заведён проект с меткой `EMPTY` +- **THEN** базовая JQL — `project = AURORA AND labels in ("EMPTY")`, метка не расширяет выборку + +#### Scenario: Та же метка другим регистром +- **WHEN** есть проект `AWPCA`, создаётся проект `awpca` +- **THEN** отказ «проект с такой меткой уже есть», проект один diff --git a/openspec/specs/hub-api/spec.md b/openspec/specs/hub-api/spec.md index fb3c219..bcf3a04 100644 --- a/openspec/specs/hub-api/spec.md +++ b/openspec/specs/hub-api/spec.md @@ -99,29 +99,36 @@ HTTP API хаба `jira-hub serve`: маршруты агента (выдача - **THEN** 422 `bad_state` с ключом Y и полем `body`; ни X, ни Y в зеркале не изменились ### Requirement: Маршруты интерфейсов -Хаб SHALL предоставлять интерфейсам (MCP, позже просмотрщик) следующие маршруты под клиентским токеном: -- `GET /issues` с фильтрами `sprint`, `assignee`, `work_kind`, `fact_status`, `status`, `label`, `epic`, `neighbor`, - `q` (подстрока темы), `hide_closed` (`1` / `true` — без задач, чей `status` входит в настройку `closed_statuses`; - задача без статуса не закрыта), `limit`/`offset` — строки зеркала (все поля `issue`) с `fact_status` и `work_kind` из заметок; - сокращение до карточки — дело интерфейса. +Хаб SHALL предоставлять интерфейсам (MCP, веб-приложение) следующие маршруты под клиентским токеном: +- `GET /issues` с фильтрами `project` (разрез по проекту, см. `data-model`, «Проекты»: свои и соседи, `neighbor` + относительно проекта; неизвестный проект — 404), `sprint`, `assignee`, `work_kind`, `fact_status`, `status`, `label`, + `epic`, `neighbor`, `q` (подстрока темы), `hide_closed` (`1` / `true` — без задач, чей `status` входит в настройку + `closed_statuses`; задача без статуса не закрыта), `limit`/`offset` — строки зеркала (все поля `issue`) с `fact_status` + и `work_kind` из заметок; сокращение до карточки — дело интерфейса. Без `project` — всё зеркало, `neighbor` — признак + зеркала (вне базовой JQL). - `GET /issues/{key}` — задача целиком: поля, `epic_summary` (тема эпика из зеркала или `null`), связи в обе стороны (`links_out: [{target_key, link_type, summary, status}]`, `links_in: [{source_key, link_type, summary, status}]`, где `summary` и `status` — связанной задачи из зеркала или `null`, если её там нет), заметки, последние 10 команд по ней, `comments: [{comment_id, author, created, updated, body}]` — все комментарии, новые сверху (по `created`, затем `comment_id`, по убыванию). - `PUT /issues/{key}/note` — заметки (частичное обновление, `updated_by` из тела). -- `POST /batches` `{title, author, commands: [...]}` → `{id, estimate_seconds}`; 422 при неизвестном типе, неверных - параметрах (в том числе неверного типа значения), временном ключе без `ensure_issue`, двух и более `ensure_issue` с - одинаковой `summary` (сравнение после обрезки пробелов по краям; в ответе перечислены повторяющиеся темы), а также - по проверкам постановки из `commands` (тип `bug` без настройки, файл вложения не загружен). +- `POST /batches` `{title, author, project?, commands: [...]}` → `{id, estimate_seconds}`; 422 при неизвестном типе, + неверных параметрах (в том числе неверного типа значения), временном ключе без `ensure_issue`, двух и более + `ensure_issue` с одинаковой `summary` (сравнение после обрезки пробелов по краям; в ответе перечислены повторяющиеся + темы), по проверкам постановки из `commands` (тип `bug` без настройки, файл вложения не загружен, доска для + `ensure_sprint`), а также по проверкам пакета проекта (см. `commands`), если передан `project`. - `POST /files` `{content}` (`content` — base64 содержимого файла) → `{file, size}`, где `file` — sha256 содержимого (64 hex-символа, нижний регистр); см. «Хранилище файлов вложений». -- `GET /batches?limit=`, `GET /batches/{id}` (с командами), `POST /batches/{id}/cancel` — отменяет `queued` команды - пакета; ответ — вид пакета с полем `still_claimed`: список `{id, type}` команд пакета в `claimed` (они будут выполнены - агентом; остановить — отмена команды с `force`). -- `POST /commands/{id}/retry` — новая команда с теми же параметрами в новом пакете «Повтор команды N». +- `GET /batches?limit=&project=` (с `project` — только пакеты этого проекта), `GET /batches/{id}` (с командами и + `project`), `POST /batches/{id}/cancel` — отменяет `queued` команды пакета; ответ — вид пакета с полем `still_claimed`: + список `{id, type}` команд пакета в `claimed` (они будут выполнены агентом; остановить — отмена команды с `force`). +- `POST /commands/{id}/retry` `{author?, project?}` — новая команда с теми же параметрами в новом пакете «Повтор + команды N»; хаб снимает с параметров то, что дописывает сам (`board_id` у `ensure_sprint`), и проверяет команду + заново. `project` передан и у исходного пакета другой проект (или его нет) — 422 «команда #N не из проекта P»; не + передан — новый пакет получает проект исходного и проходит его проверки. - `POST /commands/{id}/cancel` — только для `queued`; для `claimed` — 409, если не передан `force: true`. -- `POST /refresh` `{keys | jql | full: true}` → пакет из одного `refresh`. +- `POST /refresh` `{keys | jql | full: true}` → пакет из одного `refresh`; `full` — по базовой JQL из проектов, без + проектов — 422 «нет проектов». - `GET /agent`, `GET /sync-runs?limit=`, `GET /settings`, `PUT /settings` (частично; поднимает `settings_version`; ответ — все настройки после записи, как у `GET /settings`, включая `settings_version`). @@ -129,6 +136,30 @@ HTTP API хаба `jira-hub serve`: маршруты агента (выдача - **WHEN** `GET /issues?sprint=Спринт 2` - **THEN** только задачи с таким `sprint`, отсортированы по сроку, затем по ключу +#### Scenario: Список проекта +- **WHEN** свои задачи `STOCKS_DRRK` — AURORA-1 и AURORA-4, сосед — AURORA-2; `GET /issues?project=STOCKS_DRRK` +- **THEN** три задачи, у AURORA-2 `neighbor = true`; с `neighbor=1` — только AURORA-2; задач только с меткой `AWPCA` нет + +#### Scenario: Неизвестный проект +- **WHEN** `GET /issues?project=NOPE` +- **THEN** 404 + +#### Scenario: Пакеты проекта +- **WHEN** пакет 10 поставлен с `project = STOCKS_DRRK`, пакет 11 — без проекта; `GET /batches?project=STOCKS_DRRK` +- **THEN** в ответе только пакет 10 + +#### Scenario: Повтор чужой команды +- **WHEN** команда 40 из пакета проекта `AWPCA`; `POST /commands/40/retry {project: "STOCKS_DRRK"}` +- **THEN** 422 «команда #40 не из проекта STOCKS_DRRK», пакет не создан + +#### Scenario: Повтор ensure_sprint +- **WHEN** `ensure_sprint` 41 failed, в её `params` `board_id = 4216`; `POST /commands/41/retry` +- **THEN** новый пакет создан, `board_id` в новой команде вычислен хабом заново + +#### Scenario: Полный прогон без проектов +- **WHEN** проектов нет, `POST /refresh {full: true}` +- **THEN** 422 «нет проектов», пакет не поставлен + #### Scenario: Отмена взятой команды - **WHEN** `POST /commands/15/cancel` без `force`, команда `claimed` - **THEN** 409 с `claimed_at`; с `force: true` — `cancelled`, следующий отчёт агента по ней принимается, статус не меняется @@ -175,9 +206,9 @@ HTTP API хаба `jira-hub serve`: маршруты агента (выдача - **THEN** самые старые удаляются до `backup_keep` ### Requirement: Автообновление зеркала -Если `auto_refresh_minutes > 0` и агент на связи, хаб SHALL поставить пакет «Автообновление» из `refresh {jql: base_jql}`, -когда в очереди нет команд в `queued` и `claimed` и последний `sync_run` начат раньше, чем `auto_refresh_minutes` назад -(или `sync_run` ещё нет). +Если `auto_refresh_minutes > 0`, агент на связи и есть хотя бы один проект, хаб SHALL поставить пакет «Автообновление» +из `refresh {jql: <базовая JQL из проектов>}`, когда в очереди нет команд в `queued` и `claimed` и последний `sync_run` +начат раньше, чем `auto_refresh_minutes` назад (или `sync_run` ещё нет). #### Scenario: Очередь занята - **WHEN** интервал прошёл, но в очереди есть `queued` команды @@ -191,6 +222,10 @@ HTTP API хаба `jira-hub serve`: маршруты агента (выдача - **WHEN** очередь пуста, `auto_refresh_minutes = 60`, последний `sync_run` начат 20 минут назад - **THEN** автообновление не ставится +#### Scenario: Два проекта +- **WHEN** интервал прошёл, очередь пуста, проекты `AWPCA` и `STOCKS_DRRK` +- **THEN** поставлен один `refresh` с JQL `project = AURORA AND labels in ("AWPCA", "STOCKS_DRRK")` + ### Requirement: Реле через общую папку `jira-hub serve` SHALL запускать поток реле вместе с HTTP-сервером. Пока настройка `relay_dir` пуста (`null`), реле бездействует; настройку оно SHALL перечитывать не реже раза в 5 с, включение и выключение не требуют перезапуска. Если @@ -247,18 +282,21 @@ HTTP API хаба `jira-hub serve`: маршруты агента (выдача - **THEN** реле создаёт `resp/` заново; файл в `work/` не удалён, в логе нет «удалён незавершённый запрос» ### Requirement: Снимок доски -Хаб SHALL предоставлять `GET /board` под клиентским токеном — согласованный снимок зеркала для доски связей, прочитанный -в одной транзакции: +Хаб SHALL предоставлять `GET /board?project=` под клиентским токеном — согласованный снимок зеркала для доски связей, +прочитанный в одной транзакции: - `generated_at` — момент снимка (UTC); -- `last_sync` — последний завершённый полный прогон (`refresh` по базовой JQL): `{started_at, finished_at, issues_count}` - или `null`, если такого не было; -- `issues` — все задачи зеркала, включая эпики и соседей: `key`, `summary`, `issue_type`, `status`, `priority`, - `assignee`, `sprint`, `duedate`, `planned_start`, `planned_end`, `epic_key`, `labels` (список), `neighbor` (bool), - `fact_status`, `work_kind` (из заметок, `null` если заметки нет); порядок — по ключу; -- `links` — все связи зеркала: `{source, target, link_type}`, `link_type` — имя типа в Jira, направление как в - `issue_link` (`source` блокирует/ссылается на `target`). -Перевод в модель доски (вид работы по меткам, имена эпиков, логические типы связей) — дело интерфейса; хаб отдаёт данные -зеркала без преобразований. +- `last_sync` — последний завершённый полный прогон (`refresh` по текущей базовой JQL из проектов): + `{started_at, finished_at, issues_count}` или `null`, если такого не было; общий для всех проектов; +- `issues` — с `project` — свои задачи проекта и его соседи (см. `data-model`, «Проекты»), `neighbor` относительно + проекта; без `project` — все задачи зеркала, включая эпики и соседей, `neighbor` — признак зеркала. Поля: `key`, + `summary`, `issue_type`, `status`, `priority`, `assignee`, `sprint`, `duedate`, `planned_start`, `planned_end`, + `epic_key`, `labels` (список), `neighbor` (bool), `fact_status`, `work_kind` (из заметок, `null` если заметки нет); + порядок — по ключу; +- `links` — с `project` — связи, у которых хотя бы один конец — своя задача проекта; без `project` — все связи + зеркала: `{source, target, link_type}`, `link_type` — имя типа в Jira, направление как в `issue_link` (`source` + блокирует/ссылается на `target`). +Неизвестный проект — 404. Перевод в модель доски (вид работы по меткам, имена эпиков, логические типы связей) — дело +интерфейса; хаб отдаёт данные зеркала без преобразований. #### Scenario: Снимок с задачами и связью - **WHEN** в зеркале эпик AURORA-1351, задачи AURORA-1620 (метка `kind-dev`, эпик AURORA-1351) и AURORA-1621, связь @@ -266,6 +304,11 @@ HTTP API хаба `jira-hub serve`: маршруты агента (выдача - **THEN** `GET /board` отдаёт три задачи по ключу, у AURORA-1620 `labels = ["kind-dev"]`, `epic_key = "AURORA-1351"`, `work_kind = "разработка"`; в `links` — `{source: "AURORA-1620", target: "AURORA-1621", link_type: "ASUPR_Блокер_Начала"}` +#### Scenario: Снимок проекта +- **WHEN** AURORA-1 (метка `STOCKS_DRRK`) связана с AURORA-2 (метка `AWPCA`), AURORA-2 связана с AURORA-3 (метка `AWPCA`); + `GET /board?project=STOCKS_DRRK` +- **THEN** `issues` — AURORA-1 (`neighbor = false`) и AURORA-2 (`neighbor = true`); `links` — только AURORA-1 ↔ AURORA-2 + #### Scenario: Последний полный прогон - **WHEN** в журнале синхронизаций полный прогон, завершённый в 10:00, и после него обновление по ключам в 11:00 - **THEN** `last_sync.finished_at` — 10:00 @@ -391,3 +434,29 @@ sha256 (без расширения и имени вложения: имя за - **WHEN** в `files/` файл A старше 30 дней без ссылок, файл B старше 30 дней, на него ссылается `ensure_attachment` в `queued`, файл C загружен вчера - **THEN** после уборки удалён только A + +### Requirement: Маршруты проектов +Хаб SHALL предоставлять под клиентским токеном: +- `GET /projects` → список `{label, name, board_id, board_type, created_at, issues_count}` по `created_at`, затем `label`; + `issues_count` — задачи зеркала с меткой проекта. +- `POST /projects` `{label, name, board_id?, author?}` → 201 `{project, batch_id}`: в одной транзакции проект + записывается и ставится пакет «Новый проект <метка>» из одного `refresh` по базовой JQL, уже включающей новую метку; + автор пакета — `author` (по умолчанию `api`). Неверные поля (см. `data-model`, «Проекты») — 422 с указанием поля; + метка, совпадающая с существующей без учёта регистра, — 409. При ошибке ни проекта, ни пакета нет. + +#### Scenario: Новый проект +- **WHEN** `POST /projects {label: "STOCKS_DRRK", name: "Запасы ДРРК", board_id: 5120, author: "viewer"}`, есть проект `AWPCA` +- **THEN** 201; `GET /projects` отдаёт два проекта, у `STOCKS_DRRK` `board_type = null`; в очереди пакет автора `viewer` + с `refresh {jql: 'project = AURORA AND labels in ("AWPCA", "STOCKS_DRRK")'}` + +#### Scenario: Проект без доски +- **WHEN** `POST /projects {label: "STOCKS_DRRK", name: "Запасы ДРРК"}` +- **THEN** 201, `board_id = null` + +#### Scenario: Повтор метки +- **WHEN** `POST /projects {label: "AWPCA", name: "…"}`, проект `AWPCA` есть +- **THEN** 409, пакет не поставлен + +#### Scenario: Без названия +- **WHEN** `POST /projects {label: "STOCKS_DRRK", name: " "}` +- **THEN** 422 с указанием поля `name` diff --git a/openspec/specs/mcp/spec.md b/openspec/specs/mcp/spec.md index 2431b8a..8668e0d 100644 --- a/openspec/specs/mcp/spec.md +++ b/openspec/specs/mcp/spec.md @@ -6,9 +6,11 @@ MCP-сервер хаба (`jira-hub mcp`, stdio) для Claude: инструм ## Requirements ### Requirement: Сервер и регистрация -`jira-hub mcp` SHALL запускать MCP-сервер по stdio (пакет `mcp`), ходящий в HTTP API хаба клиентским токеном из -настроек. Регистрация — в `~/.claude.json` для проекта `awp-ca` как `jira-hub` (`uv run --project <путь> jira-hub mcp`). -Если хаб недоступен, каждый инструмент SHALL вернуть текст «хаб не запущен: jira-hub serve», а не исключение. +`jira-hub mcp [--project <метка>]` SHALL запускать MCP-сервер по stdio (пакет `mcp`), ходящий в HTTP API хаба клиентским +токеном из настроек. Общая регистрация — в user scope Claude Code как `jira-hub` (`uv run --project <путь> jira-hub mcp`), +без проекта. Репозиторий проекта SHALL регистрировать `jira-hub` в своём `.mcp.json` (project scope) с +`--project <метка>`; эта запись перекрывает общую. Если хаб недоступен, каждый инструмент SHALL вернуть текст «хаб не +запущен: jira-hub serve», а не исключение. #### Scenario: Хаб выключен - **WHEN** вызван `issues` при остановленном хабе @@ -17,7 +19,9 @@ MCP-сервер хаба (`jira-hub mcp`, stdio) для Claude: инструм ### Requirement: Инструменты | Инструмент | Параметры | Результат | |---|---|---| -| `issues` | фильтры как у `GET /issues`, `limit` (по умолчанию 50) | таблица: ключ, тема (до 80 символов), статус, срок, исполнитель, спринт, статус по факту | +| `projects` | — | таблица: метка, название, доска, тип доски («неизвестен», пока агент не определил), число своих задач; в привязанной сессии текущий проект отмечен | +| `project_create` | `label`, `name`, `board_id` (необязательно) | `POST /projects` с автором `claude`: проект, id пакета обновления и фрагмент `.mcp.json` для репозитория проекта (см. ниже); 409/422 — текст ошибки | +| `issues` | фильтры как у `GET /issues` (кроме `project` — его задаёт привязка), `limit` (по умолчанию 50) | таблица: ключ, тема (до 80 символов), статус, срок, исполнитель, спринт, статус по факту | | `issue` | `key` | поля, связи (обе стороны, с типом и направлением), заметки, последние команды, комментарии: «комментарии (N):» и 5 последних (автор, время, текст до 300 символов с «…»), «ещё M», если их больше 5; без комментариев — «комментариев нет» | | `notes_set` | `key`, подмножество полей заметок | обновлённые заметки | | `batch_create` | `title`, `commands` (список объектов по контракту `commands`; у `ensure_attachment` вместо `file` можно `path`) | `id`, `estimate_seconds`, число команд; 422 — текст ошибки валидации | @@ -31,6 +35,8 @@ MCP-сервер хаба (`jira-hub mcp`, stdio) для Claude: инструм | `sync_log` | `limit` | последние `sync_run` | | `settings_get`, `settings_set` | ключ, значение | текущее значение / новая `settings_version` | +Разрез по проекту и поведение без привязки — требование «Привязка сессии к проекту». + Описание инструмента `batch_create` SHALL перечислять параметры каждого типа команды (обязательные, необязательные в `[ ]`, формат значения) и собираться из описания параметров в коде хаба (см. `commands`), а не вручную. @@ -39,8 +45,14 @@ MCP-сервер хаба (`jira-hub mcp`, stdio) для Claude: инструм `file`; `filename` по умолчанию — имя файла из `path`. Нет файла, нет прав, не файл, больше 10 МБ — ответ инструмента с причиной и путём, пакет не ставится. Передан и `path`, и `file` — ответ «у ensure_attachment либо path, либо file». -Описание инструмента `batch_create` SHALL предупреждать, что `ensure_issue` должна нести `labels` с базовой меткой -проекта (`AWPCA`): хаб и агент метки сами не добавляют, и задача без неё выпадает из базовой JQL и из зеркала. +Описание инструмента `batch_create` SHALL сообщать, что метку проекта сессии хаб дописывает в `ensure_issue` сам, метки +других проектов запрещены, а команды над задачами вне проекта отклоняются; `board_id` для `ensure_sprint` выставляет хаб. + +Ответ `project_create` SHALL содержать фрагмент `.mcp.json` +`{"mcpServers": {"jira-hub": {"command": "uv", "args": ["run", "--project", "<корень хаба>", "jira-hub", "mcp", +"--project", "<метка>"]}}}`, где корень хаба — корень репозитория, из которого загружен пакет текущего процесса MCP +(вычисляется от расположения пакета, без зашитого пути), и указание: положить файл в корень репозитория проекта (слить +с имеющимся `.mcp.json`) и переподключить MCP (`/mcp` → reconnect, одобрить сервер проекта). Автор команд из MCP SHALL быть `claude`; из API без автора — `api`. @@ -48,6 +60,10 @@ MCP-сервер хаба (`jira-hub mcp`, stdio) для Claude: инструм - **WHEN** `batch_create {title: "1538 в спринт 2", commands: [ensure_fields{…}, ensure_link{…}, ensure_sprint{…}]}` - **THEN** пакет создан с четырьмя командами (три + `refresh`), ответ содержит оценку в минутах +#### Scenario: Проект из чата +- **WHEN** сессия без привязки, `project_create {label: "STOCKS_DRRK", name: "Запасы ДРРК", board_id: 5120}` +- **THEN** проект создан, ответ содержит id пакета обновления и фрагмент `.mcp.json` с `"--project", "STOCKS_DRRK"` + #### Scenario: Ошибка с файлом из чата - **WHEN** `batch_create {title: "Ошибка ОСВ 60", commands: [ensure_issue{temp_key: new:1, issue_type: bug, …}, ensure_attachment{key: new:1, path: "/Users/…/ОСВ сч. 60 за 2 кв. 2026.xlsx"}]}` @@ -83,3 +99,39 @@ MCP-сервер хаба (`jira-hub mcp`, stdio) для Claude: инструм #### Scenario: Задача с семью комментариями - **WHEN** `issue {key: "AURORA-9001"}`, у задачи 7 комментариев, у самого нового текст 500 символов - **THEN** ответ содержит «комментарии (7):», 5 последних комментариев (самый новый — первым, обрезан до 300 символов с «…») и «ещё 2» + +### Requirement: Привязка сессии к проекту +`jira-hub mcp --project <метка>` SHALL привязывать сервер MCP к проекту. Привязка SHALL проверяться при каждом вызове +инструмента по `GET /projects`: метки нет — ответ «проект <метка> не заведён в хабе: project_create или исправьте +--project в .mcp.json» (кроме `projects`, `project_create`). + +В привязанной к проекту P сессии: +- `issues` — `GET /issues?project=P` с остальными фильтрами; +- `issue` — любая задача по ключу; +- `notes_set` — только своя задача P (метка P; соседи — нет) или временный ключ `new:` (его задача создаётся пакетом + проекта), иначе ответ « не в проекте P»; +- `batch_create`, `command_retry` — с `project = P` (проверки пакета проекта, см. `commands`; повтор команды другого + проекта или пакета без проекта — отказ хаба); +- `batches` — `GET /batches?project=P`; `batch_status`, `batch_wait`, `batch_cancel` — по id, без ограничения; +- `refresh` — `full` — общий полный прогон всех проектов, `keys` — как раньше. + +Без привязки инструменты `issues`, `issue`, `notes_set`, `batch_create`, `command_retry`, `refresh` SHALL отвечать +«сессия не привязана к проекту: запустите MCP с --project <метка> (список — projects)» и в хаб не ходить; `projects`, +`project_create`, `agent_status`, `sync_log`, `settings_get`, `settings_set`, `batches`, `batch_status`, `batch_wait`, +`batch_cancel` работают. + +#### Scenario: Сессия без проекта +- **WHEN** MCP запущен без `--project`, вызван `issues` +- **THEN** ответ «сессия не привязана к проекту: …», запросов к `GET /issues` нет + +#### Scenario: Задачи своего проекта +- **WHEN** MCP запущен с `--project STOCKS_DRRK`, вызван `issues` +- **THEN** в таблице только свои задачи `STOCKS_DRRK` и их соседи + +#### Scenario: Создание задачи без метки +- **WHEN** сессия `STOCKS_DRRK`, `batch_create {commands: [ensure_issue {temp_key: new:1, summary: "…"}]}` без `labels` +- **THEN** пакет поставлен с `project = STOCKS_DRRK`, у `ensure_issue` `labels = ["STOCKS_DRRK"]` + +#### Scenario: Опечатка в проекте +- **WHEN** MCP запущен с `--project STOKS`, такого проекта нет; вызван `issues` +- **THEN** ответ «проект STOKS не заведён в хабе: …» diff --git a/openspec/specs/migration/spec.md b/openspec/specs/migration/spec.md index a5d9504..3aa7b78 100644 --- a/openspec/specs/migration/spec.md +++ b/openspec/specs/migration/spec.md @@ -8,13 +8,17 @@ ### Requirement: Инициализация настроек `jira-hub init --config ` SHALL перенести в `setting`: `project_key`, типы задач, `epic_name_field_id`, `fields`, `epic_link`, `priority_map`, `labels`, `status_labels`, `kind_labels`, `comment_marker`, -`pacing` (в новую форму по типам команд), `transitions.map`, `link_types`, `sprints.board_id`; сгенерировать два токена; -установить `base_jql = "project = AURORA AND labels = AWPCA"`. Поля `auth`, `base_url`, `verify_ssl`, `ca_bundle` -MUST NOT переноситься (они у агента). +`pacing` (в новую форму по типам команд), `transitions.map`, `link_types`; сгенерировать два токена; завести проект +`AWPCA` «АРМ КА» с `board_id` из `sprints.board_id` конфига (если проекта `AWPCA` ещё нет). Поля `auth`, `base_url`, +`verify_ssl`, `ca_bundle` MUST NOT переноситься (они у агента). #### Scenario: Повторный init - **WHEN** `init` вызван на уже инициализированной базе -- **THEN** настройки не перезаписываются без `--force`, токены сохраняются +- **THEN** настройки не перезаписываются без `--force`, токены сохраняются; проект `AWPCA` не дублируется + +#### Scenario: Первый init +- **WHEN** `init --config` на пустой базе, в конфиге `sprints.board_id = 4216` +- **THEN** есть проект `AWPCA` «АРМ КА» с `board_id = 4216`; в `setting` нет `base_jql` и `board_id` ### Requirement: Импорт CSV-пакета `jira-hub import <папка admin/jira-sync>` SHALL прочитать `tasks.csv`, `new_tasks.csv`, `epics.csv` и заполнить diff --git a/openspec/specs/web-app/spec.md b/openspec/specs/web-app/spec.md index e5158b3..2c1a766 100644 --- a/openspec/specs/web-app/spec.md +++ b/openspec/specs/web-app/spec.md @@ -38,13 +38,14 @@ SHALL очищать токен и показывать экран входа. ### Requirement: Оболочка и адреса Приложение SHALL открываться под префиксом `/app/` и иметь страницы `/app/board` (доска связей), `/app/issues` (список задач) и `/app/issues/{key}` (карточка задачи — окно поверх страницы, требование «Карточка задачи»); `/app` и `/app/` SHALL вести на `/app/board`, неизвестный путь — -на страницу «не найдено» со ссылками на разделы. Верхняя панель SHALL содержать переходы «Доска», «Задачи» и действие -«Сменить токен». Неожиданная ошибка отрисовки страницы SHALL показываться сообщением с кнопкой «Перезагрузить», не -пустым экраном. +на страницу «не найдено» со ссылками на разделы. Верхняя панель SHALL содержать выпадающий список проектов рядом с +названием («<метка> — <название>», текущий отмечен, в конце «+ Новый проект…»; требование «Проект в приложении»), +переходы «Доска», «Задачи» и действие «Сменить токен». Неожиданная ошибка отрисовки страницы SHALL показываться +сообщением с кнопкой «Перезагрузить», не пустым экраном. #### Scenario: Переход между разделами - **WHEN** на доске выбран раздел «Задачи» -- **THEN** адрес — `/app/issues`, показан список задач; «назад» возвращает на доску +- **THEN** адрес — `/app/issues` с текущим проектом, показан список задач; «назад» возвращает на доску ### Requirement: Ошибки хаба Приложение SHALL различать ошибки запросов к хабу: 401 — экран входа; хаб не отвечает (сеть, 5xx) — сообщение @@ -160,11 +161,11 @@ SHALL проверять агента (`GET /agent`; офлайн — преду - **THEN** доска его не опрашивает, кнопка «Обновить из Jira» доступна ### Requirement: Список задач -Страница `/app/issues` SHALL показывать задачи зеркала из `GET /issues` таблицей: ключ (открывает карточку задачи окном поверх списка), тема, +Страница `/app/issues` SHALL показывать задачи текущего проекта из `GET /issues?project=<текущий>` таблицей: ключ (открывает карточку задачи окном поверх списка), тема, тип, статус Jira, исполнитель, спринт, срок (`planned_end`, иначе `duedate`), эпик (имя эпика, иначе ключ), вид работы, -признак «сосед»; статуса по факту в списке нет. Фильтры SHALL соответствовать параметрам `GET /issues` (спринт, исполнитель, вид +признак «сосед» (относительно проекта); статуса по факту в списке нет. Фильтры SHALL соответствовать параметрам `GET /issues` (спринт, исполнитель, вид работы, статус Jira, метка, эпик, только соседи, поиск по ключу или теме, скрыть закрытые), храниться в query-строке -адреса и применяться сервером; варианты выпадающих списков SHALL браться из снимка `GET /board`; значение фильтра из адреса, которого нет среди +адреса вместе с `project` и применяться сервером; варианты выпадающих списков SHALL браться из снимка `GET /board?project=<текущий>`; значение фильтра из адреса, которого нет среди вариантов, SHALL показываться в списке с пометкой «(нет на доске)». Флажок «скрыть закрытые» SHALL быть включён по умолчанию (в адресе ничего), снятый — `closed=1` в адресе; включённый — запрос с `hide_closed=1`, кроме случая, когда в фильтре «Статус Jira» выбран статус из `closed_statuses`. Параметр `fact_status` в адресе @@ -173,7 +174,7 @@ SHALL проверять агента (`GET /agent`; офлайн — преду #### Scenario: Фильтр в адресе - **WHEN** выбран спринт «AWPCA_Спринт 2» и исполнитель -- **THEN** адрес содержит `sprint=` и `assignee=`, таблица — результат `GET /issues` с этими параметрами; открытие этого +- **THEN** адрес содержит `project=`, `sprint=` и `assignee=`, таблица — результат `GET /issues` с этими параметрами; открытие этого адреса в новой вкладке показывает ту же выборку #### Scenario: Конец списка @@ -181,7 +182,7 @@ SHALL проверять агента (`GET /agent`; офлайн — преду - **THEN** в таблице 67 задач, «Показать ещё» не показана #### Scenario: Фильтр по закрытому спринту -- **WHEN** открыт адрес `/app/issues?sprint=AWPCA_Спринт 0`, а в снимке доски такого спринта нет +- **WHEN** открыт адрес `/app/issues?project=AWPCA&sprint=AWPCA_Спринт 0`, а в снимке доски такого спринта нет - **THEN** выпадающий список «Спринт» показывает «AWPCA_Спринт 0 (нет на доске)» выбранным, таблица — по этому фильтру #### Scenario: Карточка поверх списка @@ -371,3 +372,48 @@ SHALL сообщать об этом с кнопкой «Закрыть»; ха - **WHEN** описание — `Поле data_type = null, дата 2026-10-15, оборот 18 803 903,65 - 9 351 102,89, a*b*c, UTF-8, **важно**` - **THEN** текст показан как есть, без курсива, зачёркивания, жирного и ссылок + +### Requirement: Проект в приложении +Доска и список задач SHALL показывать один проект — текущий. Текущий проект SHALL задаваться параметром `project` в +адресе `/app/board` и `/app/issues`; без параметра — последний выбранный в этом браузере (`localStorage`), если он есть +в `GET /projects`, иначе первый из `GET /projects`; выбранный проект записывается в адрес без новой записи истории. +Проекта из адреса нет в `GET /projects` — страница SHALL показывать «проект <метка> не найден» и выпадающий список +проектов вместо данных. Проектов нет — доска и список SHALL показывать приглашение создать проект («+ Новый проект…»). +Переходы «Доска» / «Задачи» SHALL сохранять текущий проект. Карточка задачи (`/app/issues/{key}`) от проекта не зависит. + +Доска SHALL загружать `GET /board?project=<текущий>`; «Обновить из Jira» — как прежде, `POST /refresh {full: true}`, +обновляет все проекты. Раскладка, дорожки и фильтры доски в `localStorage` SHALL храниться отдельно для каждого проекта; +у проекта `AWPCA` — на прежних ключах (раскладка, сделанная до проектов, сохраняется). Фильтр спринтов SHALL +показываться, только если хотя бы у одной своей задачи снимка (`neighbor = false`) есть спринт; иначе — «колонки +«Спринт» нет — показаны все задачи». + +Форма «Новый проект» (пункт «+ Новый проект…» выпадающего списка) SHALL открываться окном поверх страницы: метка, +название, номер доски Jira (необязательно; подсказка «scrum или kanban определится сам»). «Создать» — `POST /projects` +с автором `viewer`; ошибка хаба 409/422 — текст `detail` рядом с кнопкой, окно остаётся открытым; успех — окно +закрывается, текущим становится новый проект, доска подхватывает пакет обновления (`refresh` автора `viewer`) и +показывает его ход. + +#### Scenario: Первый вход после выкатки +- **WHEN** открыт `/app/board` без `project`, в `localStorage` выбора нет, проекты `AWPCA` и `STOCKS_DRRK` +- **THEN** адрес — `/app/board?project=AWPCA`, доска — `GET /board?project=AWPCA` с прежней раскладкой карточек + +#### Scenario: Переключение проекта +- **WHEN** на доске `AWPCA` в выпадающем списке выбран `STOCKS_DRRK`, затем открыт раздел «Задачи» +- **THEN** доска — `GET /board?project=STOCKS_DRRK` со своей раскладкой; адрес «Задач» — `/app/issues?project=STOCKS_DRRK` + +#### Scenario: Неизвестный проект в адресе +- **WHEN** открыт `/app/issues?project=NOPE` +- **THEN** «проект NOPE не найден» и список проектов; запроса `GET /issues` нет + +#### Scenario: Создание проекта из формы +- **WHEN** в форме введены `STOCKS_DRRK`, «Запасы ДРРК», доска пусто; нажато «Создать» +- **THEN** `POST /projects {label: "STOCKS_DRRK", name: "Запасы ДРРК", author: "viewer"}`; текущий проект — `STOCKS_DRRK`; + шапка доски показывает «пакет #N выполняется» + +#### Scenario: Метка занята +- **WHEN** в форме введена метка `awpca`, хаб ответил 409 +- **THEN** окно открыто, рядом с «Создать» — текст ошибки хаба + +#### Scenario: Канбан-проект +- **WHEN** текущий проект — `STOCKS_DRRK`, ни у одной своей задачи снимка нет спринта (у соседа из `AWPCA` спринт есть) +- **THEN** вместо фильтра спринтов — «колонки «Спринт» нет — показаны все задачи»