Andrey Epifancev d327a0488d docs(openspec): change bug-and-attachments — тип «Ошибка» и вложения (#43)
Дельты commands, hub-api, mcp, data-model, agent: issue_type bug с настройкой
issue_types.bug, хранилище файлов хаба (POST /files, GET /agent/files/{sha256}),
команда ensure_attachment, path в MCP batch_create, агент v11; заодно R-1
(#46) и M-1/M-2 из aep/jira-agent#30.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 23:42:21 +04:00

168 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

## MODIFIED Requirements
### Requirement: Маршруты агента
Хаб SHALL предоставлять агенту следующие маршруты (все под агентским токеном):
- `POST /agent/claim` `{agent_version, settings_version, last_jira_call_at, update_state, launcher_version}` — долгий
запрос до 30 с; ответ `{command: {id, type, params, pause_seconds}, settings_version, agent_release}` или `{command:
null, settings_version, agent_release}`, где `agent_release` — `{version, sha256}` опубликованной версии агента или
`null`; обновляет `agent_status` (`last_jira_call_at = null` не затирает прежнее значение). `update_state` (объект или
`null`, `error` хранится до 500 символов) и `launcher_version` (целое или `null`) необязательны; хаб SHALL хранить
последние значения в памяти процесса, значения неверного типа игнорировать, не отвергая `claim`. Если есть команда в
`claimed`, `claim` SHALL выдать её снова (повторная выдача, см. `commands`), а не следующую из очереди.
- `GET /agent/settings` — настройки для агента (без токенов).
- `GET /agent/commands/{id}` — команда по id (для восстановления из журнала), включая `pause_seconds`.
- `POST /agent/commands/{id}/result` `{status, result, error, jira_calls, rate_limited}` — идемпотентно: повторный
отчёт по команде в терминальном статусе отвечает 200 без изменений, кроме позднего отчёта `done` с `result.key` по
`ensure_issue` в `cancelled` или `failed` без результата (результат записывается, статус не меняется, заметка `new:N`
переносится на ключ). Отчёт по команде в `queued` SHALL отвергаться 409 `not_claimed`. `error` хаб SHALL обрезать до
1500 символов.
- `POST /agent/state` `{command_id, issues: [...], links: [...], neighbor_keys: [...], page, last_page}` — загрузка состояния
пачками; `neighbor_keys` — ключи вне базовой JQL, дочитанные по связям; хаб обновляет зеркало и `sync_run`. Каждая
пачка применяется независимо; `page` и `last_page` необязательны и хабом не используются. Поле задачи `comments`
необязательно; если оно есть, хаб SHALL до записи проверить его форму: список объектов, `id` — непустая строка, `body` —
строка, `author`, `created`, `updated` — строка или `null`; нарушение — 422 `bad_state` с ключом задачи и именем поля,
пачка не применяется.
- `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`
- **THEN** `claim` отдаёт команду с `AURORA-1590` в параметрах
#### Scenario: Повторный отчёт
- **WHEN** агент дважды прислал `result` для команды 15
- **THEN** второй ответ 200, статус и результат не изменились
#### Scenario: Отчёт по невыданной команде
- **WHEN** `POST /agent/commands/16/result` с `status = done`, команда 16 в `queued`
- **THEN** 409 `not_claimed`, команда осталась `queued`
#### Scenario: Длинная ошибка
- **WHEN** агент прислал `failed` с `error` длиной 5000 символов
- **THEN** в `command.error` первые 1500 символов
#### Scenario: Поздний отчёт после предела выдач
- **WHEN** `ensure_issue {temp_key: new:8, …}` переведена хабом в `failed` «агент не отчитался после 3 выдач», затем агент
прислал `done` с `result.key = AURORA-1630`
- **THEN** команда `failed`, в `result` — `AURORA-1630`; заметка `new:8` перенесена на `AURORA-1630`
#### Scenario: Публикация в ответе claim
- **WHEN** опубликована v6, агент v5 отправил `claim`, команд нет
- **THEN** ответ `{command: null, settings_version, agent_release: {version: 6, sha256: …}}`
#### Scenario: Состояние обновления от агента
- **WHEN** агент прислал в `claim` `update_state = {state: rolled_back, from_version: 6, to_version: 7, error: "код 1 через 3 с"}`
и `launcher_version = 1`
- **THEN** `GET /agent` отдаёт это `update_state`, `launcher_version = 1` и `update_hint` «откат с v7 на v6: код 1 через 3 с»
#### Scenario: Скачивание версии
- **WHEN** агентским токеном `GET /agent/release`, опубликована v6
- **THEN** 200 `{version: 6, sha256, content}`, sha256 от `content` в UTF-8 равен `sha256`; клиентским токеном — 401;
без публикации — 404 `no_release`
#### Scenario: Скачивание файла вложения
- **WHEN** агентским токеном `GET /agent/files/<sha>`, файл загружен
- **THEN** 200 `{file: <sha>, size, content}`, sha256 от декодированного `content` равен `<sha>`; клиентским токеном — 401;
файла нет — 404 `no_file`
#### Scenario: Неверный комментарий в пачке
- **WHEN** `POST /agent/state` с задачами X и Y, у Y комментарий `{id: "7", body: 42}`
- **THEN** 422 `bad_state` с ключом Y и полем `body`; ни X, ни Y в зеркале не изменились
### Requirement: Маршруты интерфейсов
Хаб SHALL предоставлять интерфейсам (MCP, позже просмотрщик) следующие маршруты под клиентским токеном:
- `GET /issues` с фильтрами `sprint`, `assignee`, `work_kind`, `fact_status`, `status`, `label`, `epic`, `neighbor`,
`q` (подстрока темы), `limit`/`offset` — строки зеркала (все поля `issue`) с `fact_status` и `work_kind` из заметок;
сокращение до карточки — дело интерфейса.
- `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 /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».
- `POST /commands/{id}/cancel` — только для `queued`; для `claimed` — 409, если не передан `force: true`.
- `POST /refresh` `{keys | jql | full: true}` → пакет из одного `refresh`.
- `GET /agent`, `GET /sync-runs?limit=`, `GET /settings`, `PUT /settings` (частично; поднимает `settings_version`;
ответ — все настройки после записи, как у `GET /settings`, включая `settings_version`).
#### Scenario: Фильтр по спринту
- **WHEN** `GET /issues?sprint=Спринт 2`
- **THEN** только задачи с таким `sprint`, отсортированы по сроку, затем по ключу
#### Scenario: Отмена взятой команды
- **WHEN** `POST /commands/15/cancel` без `force`, команда `claimed`
- **THEN** 409 с `claimed_at`; с `force: true` — `cancelled`, следующий отчёт агента по ней принимается, статус не меняется
#### Scenario: Поздний отчёт по отменённому созданию
- **WHEN** `ensure_issue {temp_key: new:7, …}` отменена с `force: true`, затем агент прислал `done` с `result.key = AURORA-1620`
- **THEN** команда `cancelled`, в `result` — `AURORA-1620`; заметка `new:7` перенесена на `AURORA-1620`
#### Scenario: Дубль темы в пакете
- **WHEN** `POST /batches` с двумя `ensure_issue`, у обеих `summary = "Шахматка: комментарии"`
- **THEN** 422 с темой «Шахматка: комментарии», пакет не создан
#### Scenario: Тема не строка
- **WHEN** `POST /batches` с `ensure_issue {temp_key: new:1, summary: 42, …}`
- **THEN** 422 с указанием параметра `summary`, пакет не создан
#### Scenario: Отмена пакета с командой у агента
- **WHEN** в пакете 9 команда 30 `claimed`, команды 31 и 32 `queued`; `POST /batches/9/cancel`
- **THEN** 31 и 32 `cancelled`; в ответе `still_claimed = [{id: 30, type: …}]`; 30 остаётся `claimed`
#### Scenario: Ответ на запись настроек
- **WHEN** `PUT /settings {relay_dir: "/Volumes/externalSSD/utm/rshb/jira-bridge"}`
- **THEN** 200; ответ содержит `relay_dir` с записанным значением, новую `settings_version` и остальные настройки;
токенов в ответе нет
#### Scenario: Карточка с комментариями и связями
- **WHEN** у AURORA-9001 два комментария (14.10 и 15.10), исходящая связь на AURORA-9002 («API свода», «Новая») и на
AURORA-1, которой нет в зеркале; эпик AURORA-9100 «Тестовый эпик»
- **THEN** `GET /issues/AURORA-9001` отдаёт `comments` с комментарием от 15.10 первым, у связи на AURORA-9002 —
`summary = "API свода"`, `status = "Новая"`, у связи на AURORA-1 — `summary = null`, `status = null`;
`epic_summary = "Тестовый эпик"`
## ADDED Requirements
### 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 «файл <sha> не загружен: 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