Proposal, design и дельты спек hub-api, data-model, commands, agent, mcp, migration; Purpose во всех шести спеках (O-4 #8). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
140 lines
13 KiB
Markdown
140 lines
13 KiB
Markdown
# hub-api Specification
|
||
|
||
## Purpose
|
||
HTTP API хаба `jira-hub serve`: маршруты агента (выдача команд, отчёты, загрузка состояния), маршруты интерфейсов (зеркало, заметки, пакеты, настройки), резервные копии, автообновление зеркала и реле общей папки для агента за VPN.
|
||
|
||
## Requirements
|
||
|
||
### Requirement: Процесс и доступ
|
||
`jira-hub serve` SHALL запускать один процесс: HTTP на `0.0.0.0:<port>` (по умолчанию 8790), база и настройки в
|
||
`~/.local/share/jira-hub/` (переопределяется `JIRA_HUB_HOME`). Все маршруты SHALL требовать `Authorization: Bearer <token>`:
|
||
агентский токен открывает только `/agent/*`, клиентский — всё остальное. Ответы — JSON, ошибки — `{error, detail}` с
|
||
кодами 400/401/404/409/422.
|
||
|
||
#### Scenario: Чужой токен
|
||
- **WHEN** агентским токеном вызван `GET /issues`
|
||
- **THEN** 401
|
||
|
||
### Requirement: Маршруты агента
|
||
Хаб SHALL предоставлять агенту следующие маршруты (все под агентским токеном):
|
||
- `POST /agent/claim` `{agent_version, settings_version, last_jira_call_at}` — долгий запрос до 30 с; ответ `{command:
|
||
{id, type, params, pause_seconds}, settings_version}` или `{command: null, settings_version}`; обновляет `agent_status`.
|
||
Если есть команда в `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`, заметка `new:N`
|
||
переносится на ключ). Отчёт по команде в `queued` SHALL отвергаться 409 `not_claimed`.
|
||
- `POST /agent/state` `{command_id, issues: [...], links: [...], neighbor_keys: [...], page, last_page}` — загрузка состояния
|
||
пачками; `neighbor_keys` — ключи вне базовой JQL, дочитанные по связям; хаб обновляет зеркало и `sync_run`.
|
||
- `GET /agent/known-keys` — ключи зеркала (для определения соседей).
|
||
|
||
#### 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`
|
||
|
||
### Requirement: Маршруты интерфейсов
|
||
Хаб SHALL предоставлять интерфейсам (MCP, позже просмотрщик) следующие маршруты под клиентским токеном:
|
||
- `GET /issues` с фильтрами `sprint`, `assignee`, `work_kind`, `fact_status`, `status`, `label`, `epic`, `neighbor`,
|
||
`q` (подстрока темы), `limit`/`offset` — компактные карточки (ключ, тема, статус, приоритет, срок, исполнитель, спринт,
|
||
эпик, заметки кратко).
|
||
- `GET /issues/{key}` — задача целиком: поля, связи в обе стороны, заметки, последние 10 команд по ней.
|
||
- `PUT /issues/{key}/note` — заметки (частичное обновление, `updated_by` из тела).
|
||
- `POST /batches` `{title, author, commands: [...]}` → `{id, estimate_seconds}`; 422 при неизвестном типе, неверных
|
||
параметрах, временном ключе без `ensure_issue`, двух и более `ensure_issue` с одинаковой `summary` (сравнение после
|
||
обрезки пробелов по краям; в ответе перечислены повторяющиеся темы).
|
||
- `GET /batches?limit=`, `GET /batches/{id}` (с командами), `POST /batches/{id}/cancel`.
|
||
- `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`).
|
||
|
||
#### 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 с темой «Шахматка: комментарии», пакет не создан
|
||
|
||
### Requirement: Резервные копии
|
||
Перед выдачей первой команды пакета, содержащего записи в Jira, хаб SHALL скопировать файл базы в `backups/<дата-время>-<batch>.sqlite`
|
||
и хранить последние `backup_keep` копий.
|
||
|
||
#### Scenario: Ротация
|
||
- **WHEN** копий больше `backup_keep`
|
||
- **THEN** самые старые удаляются до `backup_keep`
|
||
|
||
### Requirement: Автообновление зеркала
|
||
Если `auto_refresh_minutes > 0`, агент на связи и очередь пуста дольше этого интервала, хаб SHALL поставить пакет
|
||
«Автообновление» из `refresh {jql: base_jql}`. Очередь не пуста, если есть команды в `queued` или `claimed`.
|
||
|
||
#### Scenario: Очередь занята
|
||
- **WHEN** интервал прошёл, но в очереди есть `queued` команды
|
||
- **THEN** автообновление не ставится до опустошения очереди
|
||
|
||
#### Scenario: Есть выданная команда без отчёта
|
||
- **WHEN** интервал прошёл, `queued` нет, одна команда в `claimed`
|
||
- **THEN** автообновление не ставится; команда довыдаётся при следующем `claim`
|
||
|
||
### Requirement: Реле через общую папку
|
||
`jira-hub serve` SHALL запускать поток реле вместе с HTTP-сервером. Пока настройка `relay_dir` пуста (`null`), реле
|
||
бездействует; настройку оно SHALL перечитывать не реже раза в 5 с, включение и выключение не требуют перезапуска. Если
|
||
каталог `relay_dir` существует, реле SHALL создать в нём подкаталоги `req/`, `resp/`, `work/`; если не существует —
|
||
записать одно предупреждение в лог и проверять снова.
|
||
|
||
Обмен:
|
||
- запрос агента — файл `req/<id>.json`: `{id, method, path, body, authorization, deadline}`, где `deadline` — момент UTC
|
||
(ISO 8601), после которого ответ не нужен; файлы, имя которых начинается с точки, реле MUST NOT читать;
|
||
- реле SHALL брать запрос переименованием в `work/<id>.json` и обрабатывать каждый взятый запрос в отдельном потоке;
|
||
- запрос с истёкшим `deadline` SHALL выбрасываться без ответа; `POST /agent/claim` SHALL пересылаться, только если до
|
||
`deadline` осталось не меньше 35 с, иначе выбрасываться без ответа;
|
||
- остальные запросы реле SHALL пересылать в свой HTTP на `127.0.0.1:<port>` тем же методом, путём и телом, с заголовком
|
||
`Authorization` из файла, и записывать ответ `resp/<id>.json` = `{id, status, body}` (тело — строка ответа HTTP) через
|
||
временный файл с именем на точку и переименование; затем удалять `work/<id>.json`;
|
||
- если соединение с `127.0.0.1:<port>` не удалось, ответ SHALL иметь `status = 502` и текст ошибки в `body`;
|
||
- нечитаемый JSON запроса SHALL перечитываться до 2 с, затем запрос выбрасывается с записью в лог.
|
||
|
||
При старте реле SHALL удалить файлы из `work/` с записью в лог. Раз в минуту реле SHALL удалять ответы в `resp/` старше
|
||
10 мин, запросы в `req/` с `deadline` старше 60 с и файлы с именем на точку старше 10 мин. На каждый обработанный запрос
|
||
реле SHALL писать в лог хаба строку `relay <METHOD> <path> <status> <секунды>`. HTTP API, обработчики и проверка токенов
|
||
при этом не меняются.
|
||
|
||
#### Scenario: Запрос через папку
|
||
- **WHEN** в `req/` появился `a1.json` с `GET /agent/settings`, агентским токеном и `deadline` через 60 с
|
||
- **THEN** в `resp/` появляется `a1.json` со `status = 200` и телом настроек; `work/a1.json` удалён
|
||
|
||
#### Scenario: Чужой токен через папку
|
||
- **WHEN** в файле запроса `GET /issues` с агентским токеном
|
||
- **THEN** ответ со `status = 401`, как по HTTP
|
||
|
||
#### Scenario: Просроченный claim
|
||
- **WHEN** реле взяло `POST /agent/claim`, до `deadline` которого осталось 20 с
|
||
- **THEN** запрос выброшен без ответа и без обращения к хабу; команда в очереди осталась `queued`
|
||
|
||
#### Scenario: Долгий claim не задерживает другие запросы
|
||
- **WHEN** реле пересылает `claim`, который ждёт команду 30 с, и в это время в `req/` появляется `GET /agent/settings`
|
||
- **THEN** ответ на `settings` пишется, не дожидаясь `claim`
|
||
|
||
#### Scenario: Папка не подключена
|
||
- **WHEN** `relay_dir` задан, но каталога нет; через минуту каталог появился
|
||
- **THEN** в логе одно предупреждение; после появления каталога реле создаёт подкаталоги и обрабатывает запросы
|