From f92415359690b0da0a00f004c10a35dc5496a090 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Fri, 16 Oct 2026 10:10:59 +0400 Subject: [PATCH] =?UTF-8?q?docs(openspec):=20=D0=B0=D1=80=D1=85=D0=B8?= =?UTF-8?q?=D0=B2=20change=20bug-and-attachments=20(#43)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- .../.openspec.yaml | 0 .../2026-10-16-bug-and-attachments}/design.md | 0 .../proposal.md | 0 .../specs/agent/spec.md | 0 .../specs/commands/spec.md | 0 .../specs/data-model/spec.md | 0 .../specs/hub-api/spec.md | 0 .../specs/mcp/spec.md | 0 openspec/specs/agent/spec.md | 49 ++++++++++++++- openspec/specs/commands/spec.md | 60 +++++++++++++++++-- openspec/specs/data-model/spec.md | 9 ++- openspec/specs/hub-api/spec.md | 42 ++++++++++++- openspec/specs/mcp/spec.md | 20 ++++++- 13 files changed, 172 insertions(+), 8 deletions(-) rename openspec/changes/{bug-and-attachments => archive/2026-10-16-bug-and-attachments}/.openspec.yaml (100%) rename openspec/changes/{bug-and-attachments => archive/2026-10-16-bug-and-attachments}/design.md (100%) rename openspec/changes/{bug-and-attachments => archive/2026-10-16-bug-and-attachments}/proposal.md (100%) rename openspec/changes/{bug-and-attachments => archive/2026-10-16-bug-and-attachments}/specs/agent/spec.md (100%) rename openspec/changes/{bug-and-attachments => archive/2026-10-16-bug-and-attachments}/specs/commands/spec.md (100%) rename openspec/changes/{bug-and-attachments => archive/2026-10-16-bug-and-attachments}/specs/data-model/spec.md (100%) rename openspec/changes/{bug-and-attachments => archive/2026-10-16-bug-and-attachments}/specs/hub-api/spec.md (100%) rename openspec/changes/{bug-and-attachments => archive/2026-10-16-bug-and-attachments}/specs/mcp/spec.md (100%) diff --git a/openspec/changes/bug-and-attachments/.openspec.yaml b/openspec/changes/archive/2026-10-16-bug-and-attachments/.openspec.yaml similarity index 100% rename from openspec/changes/bug-and-attachments/.openspec.yaml rename to openspec/changes/archive/2026-10-16-bug-and-attachments/.openspec.yaml diff --git a/openspec/changes/bug-and-attachments/design.md b/openspec/changes/archive/2026-10-16-bug-and-attachments/design.md similarity index 100% rename from openspec/changes/bug-and-attachments/design.md rename to openspec/changes/archive/2026-10-16-bug-and-attachments/design.md diff --git a/openspec/changes/bug-and-attachments/proposal.md b/openspec/changes/archive/2026-10-16-bug-and-attachments/proposal.md similarity index 100% rename from openspec/changes/bug-and-attachments/proposal.md rename to openspec/changes/archive/2026-10-16-bug-and-attachments/proposal.md diff --git a/openspec/changes/bug-and-attachments/specs/agent/spec.md b/openspec/changes/archive/2026-10-16-bug-and-attachments/specs/agent/spec.md similarity index 100% rename from openspec/changes/bug-and-attachments/specs/agent/spec.md rename to openspec/changes/archive/2026-10-16-bug-and-attachments/specs/agent/spec.md diff --git a/openspec/changes/bug-and-attachments/specs/commands/spec.md b/openspec/changes/archive/2026-10-16-bug-and-attachments/specs/commands/spec.md similarity index 100% rename from openspec/changes/bug-and-attachments/specs/commands/spec.md rename to openspec/changes/archive/2026-10-16-bug-and-attachments/specs/commands/spec.md diff --git a/openspec/changes/bug-and-attachments/specs/data-model/spec.md b/openspec/changes/archive/2026-10-16-bug-and-attachments/specs/data-model/spec.md similarity index 100% rename from openspec/changes/bug-and-attachments/specs/data-model/spec.md rename to openspec/changes/archive/2026-10-16-bug-and-attachments/specs/data-model/spec.md diff --git a/openspec/changes/bug-and-attachments/specs/hub-api/spec.md b/openspec/changes/archive/2026-10-16-bug-and-attachments/specs/hub-api/spec.md similarity index 100% rename from openspec/changes/bug-and-attachments/specs/hub-api/spec.md rename to openspec/changes/archive/2026-10-16-bug-and-attachments/specs/hub-api/spec.md diff --git a/openspec/changes/bug-and-attachments/specs/mcp/spec.md b/openspec/changes/archive/2026-10-16-bug-and-attachments/specs/mcp/spec.md similarity index 100% rename from openspec/changes/bug-and-attachments/specs/mcp/spec.md rename to openspec/changes/archive/2026-10-16-bug-and-attachments/specs/mcp/spec.md diff --git a/openspec/specs/agent/spec.md b/openspec/specs/agent/spec.md index 43141dd..dbc1d2e 100644 --- a/openspec/specs/agent/spec.md +++ b/openspec/specs/agent/spec.md @@ -117,7 +117,9 @@ Jira в текущем процессе, до него — `null`. На HTTP 429 `{id, author, created, updated, body}`: `id` — строка; `author` — отображаемое имя автора, иначе его логин, иначе `null`; `created`, `updated` — время Jira как есть; `body` — текст как есть (пустой — `""`). Если `search` отдал у задачи меньше комментариев, чем `comment.total`, агент SHALL дочитать комментарии этой задачи через `GET /issue/{key}/comment` -постранично (`maxResults = 100`) и передать полный список. +постранично (`maxResults = 100`) и передать полный список. Комментарий без `id` или с пустым `id` агент SHALL отбросить +с записью в лог (ключ задачи), остальные передать. Если дочитывание комментариев задачи вернуло 403 или 404, агент SHALL +передать задачу без поля `comments` (хаб оставит прежние комментарии) с записью в лог; `refresh` продолжается. #### Scenario: Полная синхронизация - **WHEN** `refresh {jql: "project = AURORA AND labels = AWPCA"}` при 180 задачах @@ -138,6 +140,14 @@ Jira в текущем процессе, до него — `null`. На HTTP 429 - **WHEN** `search` вернул у AURORA-1620 20 комментариев при `comment.total = 130` - **THEN** агент дочитал комментарии двумя вызовами `GET /issue/AURORA-1620/comment`; в состоянии 130 комментариев +#### Scenario: Комментарий без id +- **WHEN** у задачи два комментария, у второго `id = null` +- **THEN** в состоянии один комментарий; в логе строка с ключом задачи; пачка принята хабом + +#### Scenario: Нет доступа к комментариям +- **WHEN** `GET /issue/AURORA-1620/comment` при дочитывании вернул 403 +- **THEN** AURORA-1620 в состоянии без поля `comments`; остальные задачи пачки с комментариями; `refresh` — `done` + #### Scenario: Задача без комментариев - **WHEN** у задачи в Jira нет комментариев - **THEN** в её состоянии `comments = []` @@ -345,3 +355,40 @@ README агента SHALL описывать: первую установку ( #### Scenario: Второй запускатель - **WHEN** `run.py` работает, и запускается ещё один `run.py` в том же каталоге - **THEN** второй пишет «запускатель уже работает» и завершается с кодом 1, агента не запускает + +### Requirement: Тип задачи и вложения +Агент (с v11) SHALL брать тип для `ensure_issue` из `issue_types.`: `{id}`, если `id` задан, иначе `{name}`; +нет записи или в ней нет ни `id`, ни `name` — ошибка конфигурации «нет настройки issue_types.», `failed` +без вызова Jira. + +`ensure_attachment {key, file, filename, size}` (`size` дописан хабом при выдаче) агент SHALL выполнять так: +0. `size = null` — `failed` «файл удалён с хаба, загрузите заново» без вызова Jira. +1. `GET /rest/api/2/issue/{key}?fields=attachment`; вложение с тем же `filename` и тем же `size` есть — `done` с его + `attachment_id`, файл у хаба не запрашивается. +2. Файл — `GET /agent/files/{file}`; sha256 и длина декодированного `content` SHALL совпасть с `file` и `size`, иначе — + временный сбой (повтор, как обрыв связи с хабом); 404 — `failed` «файл удалён с хаба, загрузите заново». +3. `POST /rest/api/2/issue/{key}/attachments`, `multipart/form-data` с одной частью `file`, имя файла — `filename` в UTF-8 + (`filename="…"` и `filename*=UTF-8''…`), заголовок `X-Atlassian-Token: no-check`; результат `attachment_id` из ответа. + Ответ Jira 4xx (в том числе 413) — `failed` с кодом и текстом Jira. +Содержимое файла MUST NOT попадать в журнал и лог (только `file`, `filename`, размер). `AGENT_VERSION` SHALL быть 11. + +#### Scenario: Ошибка по имени типа +- **WHEN** `ensure_issue {issue_type: bug, …}`, `issue_types.bug = {name: "Ошибка"}` +- **THEN** в `POST /issue` `issuetype = {name: "Ошибка"}` + +#### Scenario: Нет настройки типа +- **WHEN** `ensure_issue {issue_type: bug, …}`, в `issue_types` нет `bug` +- **THEN** `failed` «нет настройки issue_types.bug», вызовов Jira нет + +#### Scenario: Загрузка вложения с кириллическим именем +- **WHEN** `ensure_attachment {key: X, file: , filename: "ОСВ сч. 60 за 2 кв. 2026.xlsx"}`, у X вложений нет +- **THEN** один `GET /issue/X`, один `GET /agent/files/`, один `POST /issue/X/attachments`; в Jira вложение с этим + именем и размером файла; отчёт `done` с `attachment_id` + +#### Scenario: Файл повреждён в пути +- **WHEN** sha256 полученного `content` не равен `file` +- **THEN** вложение в Jira не загружается; команда повторяется как при обрыве связи с хабом + +#### Scenario: Старый агент +- **WHEN** агент v10 получил `ensure_attachment` +- **THEN** `failed` «неизвестный тип команды», агент работает дальше (поведение v10, без изменений) diff --git a/openspec/specs/commands/spec.md b/openspec/specs/commands/spec.md index dd2fc2a..708655b 100644 --- a/openspec/specs/commands/spec.md +++ b/openspec/specs/commands/spec.md @@ -12,12 +12,13 @@ | Тип | Параметры | Что читает | Что пишет | |---|---|---|---| -| `ensure_issue` | `temp_key` (`new:`), `issue_type` (`task`/`epic`), `summary`, `description`, `priority`, `duedate`, `planned_start`, `planned_end`, `labels`, `epic_key`, `assignee` | поиск `project = AND reporter = currentUser() AND created >= -7d`, точное сравнение `summary` | при отсутствии — `POST /issue` (для эпика — с `epic_name_field_id`); результат `key` | +| `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` | `GET /issue/{key}` | `PUT /issue/{key}` только с отличающимися полями; эпик — по `epic_link.mode` | | `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_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; состояние уходит в хаб | Семантика `ensure_link`: `source` блокирует/ссылается на `target`; агент в `POST /issueLink` SHALL ставить `source` в @@ -48,6 +49,20 @@ - **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` с ключом найденной @@ -96,7 +111,7 @@ ### Requirement: Политика темпа и пауза на команде Хаб SHALL хранить `pacing` как минимальные зазоры в секундах по типу команды (по умолчанию: `ensure_issue` и -`ensure_fields` 120, `ensure_comment`, `ensure_status`, `ensure_link`, `ensure_sprint` 30, `refresh` 2). При выдаче команды +`ensure_fields` 120, `ensure_comment`, `ensure_status`, `ensure_link`, `ensure_sprint`, `ensure_attachment` 30, `refresh` 2). При выдаче команды хаб SHALL сообщить `pause_seconds` = зазор по типу выдаваемой команды; агент SHALL выдержать его от времени своего последнего вызова Jira. Внутри одной команды с несколькими записями (`ensure_link` с `replace`, путь переходов) агент SHALL выдерживать зазор типа команды между записями. @@ -122,8 +137,8 @@ SHALL выдерживать зазор типа команды между за **Подтверждение по состоянию.** Если после любого `refresh` желаемый результат команды в `claimed` виден в зеркале, хаб SHALL перевести её в `confirmed_by_state`: `ensure_fields` — все поля совпадают (строки сравниваются без различия `\r\n` и `\n`; `null` и пустая строка равны); `ensure_status` — статус равен последнему в пути; `ensure_link` — связь -есть и конфликтов нет; `ensure_sprint` — спринт совпадает. `ensure_issue`, `ensure_comment` и `refresh` по состоянию не -подтверждаются: потерянный отчёт по ним закрывается повторной выдачей (агент находит созданную задачу поиском дубля) +есть и конфликтов нет; `ensure_sprint` — спринт совпадает. `ensure_issue`, `ensure_comment`, `ensure_attachment` и `refresh` по +состоянию не подтверждаются (вложений в зеркале нет): потерянный отчёт по ним закрывается повторной выдачей (агент находит созданную задачу поиском дубля) или отменой человеком. #### Scenario: Отчёт потерян, факт на месте @@ -146,3 +161,40 @@ SHALL перевести её в `confirmed_by_state`: `ensure_fields` — вс - **WHEN** `ensure_issue {temp_key: new:5, summary: "Отчёт"}` выполнена агентом, отчёт не дошёл, агент прислал `claim` - **THEN** команда выдаётся снова; агент находит созданную задачу поиском дубля и отчитывается `done` с её ключом; второй задачи нет, заметка `new:5` перенесена на ключ + +### Requirement: Описание параметров и ошибки валидации +Описание параметров каждого типа команды (обязательные, необязательные, формат значения, условие на набор) SHALL +храниться в коде хаба в одном месте с правилами валидации; описание инструмента MCP `batch_create` SHALL собираться из +него (см. `mcp`). Ошибка валидации «нет <параметры>» и «лишние параметры <параметры>» SHALL заканчиваться ожидаемым +набором параметров типа: «ожидаются <обязательные>» и, если у типа есть необязательные, «необязательные <список>». + +#### Scenario: Неверное имя параметра +- **WHEN** `POST /batches` с `ensure_status {key: X, status: "Готово"}` +- **THEN** 422 «команда 1 (ensure_status): нет target; ожидаются key, target» + +#### Scenario: Лишний параметр у типа с необязательными +- **WHEN** `POST /batches` с `ensure_link {source: X, target: Y, link_type: "связанные", force: true}` +- **THEN** 422 «команда 1 (ensure_link): лишние параметры force; ожидаются source, target, link_type; необязательные replace» + +### Requirement: Проверки ensure_issue с типом bug и ensure_attachment при постановке +При постановке пакета хаб SHALL отвергать 422 без создания пакета: `ensure_issue` с `issue_type = bug`, если в настройках +нет `issue_types.bug` с непустым `name` или `id` («нет настройки issue_types.bug»); `ensure_attachment`, если `file` — не +64 hex-символа в нижнем регистре, файла с таким sha256 нет в хранилище хаба («файл не загружен: POST /files»), +`filename` пустой (или из одних пробелов) или содержит `/`, `\`, `"` или управляющие символы (код < 32 и 127, +в том числе CR и LF): такие имена ломают `multipart/form-data`. Клиент `size` не передаёт (лишний параметр — 422). При выдаче агенту +(`claim`, в том числе повторной) хаб SHALL добавить к параметрам `ensure_attachment` `size` — текущий размер файла в +хранилище, или `size = null`, если файла уже нет; в базе параметры команды остаются без `size`, поэтому повтор команды +(`POST /commands/{id}/retry`) проходит проверку как новая. + +#### Scenario: Ошибка без настройки типа +- **WHEN** `POST /batches` с `ensure_issue {issue_type: bug, …}`, в `issue_types` только `task` и `epic` +- **THEN** 422 «команда 1: нет настройки issue_types.bug», пакет не создан + +#### Scenario: Вложение без загруженного файла +- **WHEN** `POST /batches` с `ensure_attachment {key: X, file: <64 hex>, filename: "a.xlsx"}`, файла нет в хранилище +- **THEN** 422 «команда 1: файл не загружен: POST /files», пакет не создан + +#### Scenario: Ошибка с вложением одним пакетом +- **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` файла diff --git a/openspec/specs/data-model/spec.md b/openspec/specs/data-model/spec.md index 8d592b2..dd0a3dc 100644 --- a/openspec/specs/data-model/spec.md +++ b/openspec/specs/data-model/spec.md @@ -132,12 +132,19 @@ SHALL обнулить `issues_count` и `links_count` её `sync_run` (`started правке), `backup_keep`, `auto_refresh_minutes`, `relay_dir` (абсолютный путь папки реле или `null`, по умолчанию `null`; относительный путь и не-строка при записи — 422; агенту не передаётся). Спринты агент читает с доски `board_id`, отдельной настройки имён спринтов нет. Начальные значения SHALL импортироваться из `admin/jira-sync/config.example.json` -командой `jira-hub init`; пароль и логин Jira в настройках MUST NOT присутствовать. +командой `jira-hub init`; пароль и логин Jira в настройках MUST NOT присутствовать. `issue_types` — объект +`{task, epic, bug}`, у каждого `{name, id}` (`id` может быть `null`); `init` берёт их из ключей конфига +`issue_type_<тип>` и `issue_type_<тип>_id`; `bug` попадает в `issue_types`, только если в конфиге есть хотя бы +один из ключей `issue_type_bug`, `issue_type_bug_id` (`task` и `epic` — как раньше, всегда). #### Scenario: Правка настроек поднимает версию - **WHEN** через API изменён `pacing` - **THEN** `settings_version` увеличен, следующий `claim` отдаёт агенту новую версию +#### Scenario: Конфиг без ключей ошибки +- **WHEN** `jira-hub init --config` с `issue_type_task`, `issue_type_epic`, без `issue_type_bug` +- **THEN** `issue_types` содержит `task` и `epic`, `bug` нет; постановка `ensure_issue` с `issue_type = bug` — 422 + #### Scenario: Неверный relay_dir - **WHEN** `PUT /settings` с `relay_dir = "rshb/jira-bridge"` - **THEN** 422, значение не записано, `settings_version` не изменился diff --git a/openspec/specs/hub-api/spec.md b/openspec/specs/hub-api/spec.md index 6a1b478..3abd989 100644 --- a/openspec/specs/hub-api/spec.md +++ b/openspec/specs/hub-api/spec.md @@ -51,6 +51,8 @@ HTTP API хаба `jira-hub serve`: маршруты агента (выдача - `GET /agent/known-keys` — ключи зеркала (для определения соседей). - `GET /agent/release` — опубликованная версия агента `{version, sha256, content}` (`content` — текст `agent.py`); нет публикации или файл не сходится с манифестом по sha256 — 404 `no_release`. +- `GET /agent/files/{sha256}` — файл вложения из хранилища хаба `{file, size, content}` (`content` — base64 содержимого); + файла нет — 404 `no_file`. #### Scenario: Выдача с подстановкой ключа - **WHEN** следующая команда ссылается на `new:60`, а `ensure_issue` для `new:60` завершилась с `AURORA-1590` @@ -87,6 +89,11 @@ HTTP API хаба `jira-hub serve`: маршруты агента (выдача - **THEN** 200 `{version: 6, sha256, content}`, sha256 от `content` в UTF-8 равен `sha256`; клиентским токеном — 401; без публикации — 404 `no_release` +#### Scenario: Скачивание файла вложения +- **WHEN** агентским токеном `GET /agent/files/`, файл загружен +- **THEN** 200 `{file: , size, content}`, sha256 от декодированного `content` равен ``; клиентским токеном — 401; + файла нет — 404 `no_file` + #### Scenario: Неверный комментарий в пачке - **WHEN** `POST /agent/state` с задачами X и Y, у Y комментарий `{id: "7", body: 42}` - **THEN** 422 `bad_state` с ключом Y и полем `body`; ни X, ни Y в зеркале не изменились @@ -104,7 +111,10 @@ HTTP API хаба `jira-hub serve`: маршруты агента (выдача - `PUT /issues/{key}/note` — заметки (частичное обновление, `updated_by` из тела). - `POST /batches` `{title, author, commands: [...]}` → `{id, estimate_seconds}`; 422 при неизвестном типе, неверных параметрах (в том числе неверного типа значения), временном ключе без `ensure_issue`, двух и более `ensure_issue` с - одинаковой `summary` (сравнение после обрезки пробелов по краям; в ответе перечислены повторяющиеся темы). + одинаковой `summary` (сравнение после обрезки пробелов по краям; в ответе перечислены повторяющиеся темы), а также + по проверкам постановки из `commands` (тип `bug` без настройки, файл вложения не загружен). +- `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`). @@ -345,3 +355,33 @@ published_at}` или `null`), `update_state`, `launcher_version`, `agent_report #### Scenario: Нулевой байт в пути - **WHEN** запрошен `GET /app/assets/%00` или `GET /app/assets/a%00.js` - **THEN** 404 `not_found`, а не 500 + +### Requirement: Хранилище файлов вложений +Хаб SHALL хранить файлы вложений в каталоге `files/` внутри каталога данных, по одному файлу на содержимое с именем +sha256 (без расширения и имени вложения: имя задаёт команда). Каталог SHALL иметь права `0700` (выставляются при создании +и при каждом старте, как у `backups/`). `POST /files`: +- `content` не строка или не base64 — 400 `bad_request`; пустой файл — 422 `empty_file`; +- файл больше 10 МБ (10 485 760 байт) после декодирования — 413 `too_large` (тело запроса до 16 МБ проходит); +- запись атомарная: временный файл в `files/`, затем переименование; повторная загрузка того же содержимого отвечает тем + же `file` и не создаёт второй копии. +Файлы в бэкапы базы MUST NOT входить. Фоновый `tick` хаба SHALL не чаще раза в сутки в пределах работы процесса (после старта — на первом тике) удалять файлы старше 30 дней (по +времени изменения), на которые не ссылается ни одна команда `ensure_attachment` в `queued` или `claimed`; повтор команды +после уборки отвергается при постановке 422 «файл не загружен: POST /files». Повторная загрузка того же +содержимого SHALL обновлять время изменения файла. + +#### Scenario: Загрузка файла +- **WHEN** `POST /files {content: base64("abc")}` +- **THEN** 200 `{file: "ba7816bf…", size: 3}`; в `files/` файл `ba7816bf…` с содержимым `abc` + +#### Scenario: Повторная загрузка +- **WHEN** тот же `POST /files` второй раз +- **THEN** 200 с тем же `file`; в `files/` по-прежнему один файл + +#### Scenario: Слишком большой файл +- **WHEN** `POST /files` с файлом 11 МБ +- **THEN** 413 `too_large`, в `files/` ничего не появилось + +#### Scenario: Уборка старых файлов +- **WHEN** в `files/` файл A старше 30 дней без ссылок, файл B старше 30 дней, на него ссылается `ensure_attachment` в + `queued`, файл C загружен вчера +- **THEN** после уборки удалён только A diff --git a/openspec/specs/mcp/spec.md b/openspec/specs/mcp/spec.md index bf7004e..2431b8a 100644 --- a/openspec/specs/mcp/spec.md +++ b/openspec/specs/mcp/spec.md @@ -20,7 +20,7 @@ MCP-сервер хаба (`jira-hub mcp`, stdio) для Claude: инструм | `issues` | фильтры как у `GET /issues`, `limit` (по умолчанию 50) | таблица: ключ, тема (до 80 символов), статус, срок, исполнитель, спринт, статус по факту | | `issue` | `key` | поля, связи (обе стороны, с типом и направлением), заметки, последние команды, комментарии: «комментарии (N):» и 5 последних (автор, время, текст до 300 символов с «…»), «ещё M», если их больше 5; без комментариев — «комментариев нет» | | `notes_set` | `key`, подмножество полей заметок | обновлённые заметки | -| `batch_create` | `title`, `commands` (список объектов по контракту `commands`) | `id`, `estimate_seconds`, число команд; 422 — текст ошибки валидации | +| `batch_create` | `title`, `commands` (список объектов по контракту `commands`; у `ensure_attachment` вместо `file` можно `path`) | `id`, `estimate_seconds`, число команд; 422 — текст ошибки валидации | | `batch_status` | `id` | статусы команд, ошибки, оценка остатка | | `batch_wait` | `id`, `timeout_minutes` (≤ 60) | как `batch_status`, возвращается по завершении или по таймауту | | `batches` | `limit` | последние пакеты со статусом | @@ -31,6 +31,14 @@ MCP-сервер хаба (`jira-hub mcp`, stdio) для Claude: инструм | `sync_log` | `limit` | последние `sync_run` | | `settings_get`, `settings_set` | ключ, значение | текущее значение / новая `settings_version` | +Описание инструмента `batch_create` SHALL перечислять параметры каждого типа команды (обязательные, необязательные в +`[ ]`, формат значения) и собираться из описания параметров в коде хаба (см. `commands`), а не вручную. + +`ensure_attachment` в `batch_create` SHALL принимать вместо `file` параметр `path` — абсолютный путь к локальному файлу на +машине MCP-сервера. До постановки пакета MCP SHALL прочитать файл, загрузить его `POST /files` и заменить `path` на +`file`; `filename` по умолчанию — имя файла из `path`. Нет файла, нет прав, не файл, больше 10 МБ — ответ инструмента +с причиной и путём, пакет не ставится. Передан и `path`, и `file` — ответ «у ensure_attachment либо path, либо file». + Описание инструмента `batch_create` SHALL предупреждать, что `ensure_issue` должна нести `labels` с базовой меткой проекта (`AWPCA`): хаб и агент метки сами не добавляют, и задача без неё выпадает из базовой JQL и из зеркала. @@ -40,6 +48,16 @@ MCP-сервер хаба (`jira-hub mcp`, stdio) для Claude: инструм - **WHEN** `batch_create {title: "1538 в спринт 2", commands: [ensure_fields{…}, ensure_link{…}, ensure_sprint{…}]}` - **THEN** пакет создан с четырьмя командами (три + `refresh`), ответ содержит оценку в минутах +#### 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"}]}` +- **THEN** файл загружен в хаб; пакет создан с `ensure_attachment {key: new:1, file: , filename: "ОСВ сч. 60 за 2 кв. + 2026.xlsx"}` и хвостовым `refresh` + +#### Scenario: Файла нет +- **WHEN** тот же вызов, файла по `path` нет +- **THEN** ответ «файл не найден: », пакет не создан, в хаб ничего не загружено + #### Scenario: Ожидание результата - **WHEN** `batch_wait {id, timeout_minutes: 10}` и пакет завершился через 6 минут - **THEN** ответ через 6 минут со статусами всех команд