# hub-api Specification ## Purpose HTTP API хаба `jira-hub serve`: маршруты агента (выдача команд, отчёты, загрузка состояния), маршруты интерфейсов (зеркало, заметки, пакеты, настройки), резервные копии, автообновление зеркала и реле общей папки для агента за VPN. ## Requirements ### Requirement: Процесс и доступ `jira-hub serve` SHALL запускать один процесс: HTTP на `0.0.0.0:` (по умолчанию 8790), база и настройки в `~/.local/share/jira-hub/` (переопределяется `JIRA_HUB_HOME`). Все маршруты SHALL требовать `Authorization: Bearer `: агентский токен открывает только `/agent/*`, клиентский — всё остальное. Ответы — JSON, ошибки — `{error, detail}` с кодами 400 (неверное тело или `Content-Length`), 401, 404, 409, 413 (тело больше предела), 422, 500 (внутренняя ошибка); реле общей папки дополнительно отвечает 502. Методы, кроме GET, POST и PUT, отвечают 501 стандартной страницей `http.server`, не JSON. #### Scenario: Чужой токен - **WHEN** агентским токеном вызван `GET /issues` - **THEN** 401 #### Scenario: Неподдерживаемый метод - **WHEN** `DELETE /issues/AURORA-1` - **THEN** 501, тело не JSON; база не меняется ### 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` (`last_jira_call_at = null` не затирает прежнее значение). Если есть команда в `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` необязательны и хабом не используются. - `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` #### 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` ### 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}` — задача целиком: поля, связи в обе стороны, заметки, последние 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` — отменяет `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`). #### 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` ### Requirement: Резервные копии Перед выдачей первой команды пакета, содержащего записи в Jira, хаб SHALL скопировать файл базы в `backups/<дата-время>-.sqlite` и хранить последние `backup_keep` копий. #### Scenario: Ротация - **WHEN** копий больше `backup_keep` - **THEN** самые старые удаляются до `backup_keep` ### Requirement: Автообновление зеркала Если `auto_refresh_minutes > 0` и агент на связи, хаб SHALL поставить пакет «Автообновление» из `refresh {jql: base_jql}`, когда в очереди нет команд в `queued` и `claimed` и последний `sync_run` начат раньше, чем `auto_refresh_minutes` назад (или `sync_run` ещё нет). #### Scenario: Очередь занята - **WHEN** интервал прошёл, но в очереди есть `queued` команды - **THEN** автообновление не ставится до опустошения очереди #### Scenario: Есть выданная команда без отчёта - **WHEN** интервал прошёл, `queued` нет, одна команда в `claimed` - **THEN** автообновление не ставится; команда довыдаётся при следующем `claim` #### Scenario: Недавнее обновление - **WHEN** очередь пуста, `auto_refresh_minutes = 60`, последний `sync_run` начат 20 минут назад - **THEN** автообновление не ставится ### Requirement: Реле через общую папку `jira-hub serve` SHALL запускать поток реле вместе с HTTP-сервером. Пока настройка `relay_dir` пуста (`null`), реле бездействует; настройку оно SHALL перечитывать не реже раза в 5 с, включение и выключение не требуют перезапуска. Если каталог `relay_dir` существует, реле SHALL создать в нём подкаталоги `req/`, `resp/`, `work/`; если не существует — записать одно предупреждение в лог и проверять снова. Обмен: - запрос агента — файл `req/.json`: `{id, method, path, body, authorization, deadline}`, где `deadline` — момент UTC (ISO 8601), после которого ответ не нужен; файлы, имя которых начинается с точки, реле MUST NOT читать; - реле SHALL брать запрос переименованием в `work/.json` и обрабатывать каждый взятый запрос в отдельном потоке; - запрос с истёкшим `deadline` SHALL выбрасываться без ответа; `POST /agent/claim` SHALL пересылаться, только если до `deadline` осталось не меньше 35 с, иначе выбрасываться без ответа; - остальные запросы реле SHALL пересылать в свой HTTP на `:` процесса `serve` (`127.0.0.1`, если `serve` слушает `0.0.0.0`) тем же методом, путём и телом, с заголовком `Authorization` из файла, и записывать ответ `resp/.json` = `{id, status, body}` (тело — строка ответа HTTP) через временный файл с именем на точку и переименование; затем удалять `work/.json`; - если соединение со своим HTTP не удалось или ответ нарушает протокол HTTP, ответ SHALL иметь `status = 502` и текст ошибки в `body`; - нечитаемый JSON запроса SHALL перечитываться до 2 с, затем запрос выбрасывается с записью в лог. При первой подготовке каждой папки после старта реле (в том числе новой папки после смены `relay_dir`) реле SHALL удалить файлы из `work/` с записью в лог. Повторная подготовка той же папки (пропал подкаталог, не было доступа) SHALL только создавать недостающие подкаталоги, не трогая `work/`. Раз в минуту реле SHALL удалять ответы в `resp/` старше 10 мин, запросы в `req/` с `deadline` старше 60 с и файлы с именем на точку старше 10 мин. На каждый обработанный запрос реле SHALL писать в лог хаба строку `relay <секунды>`. 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** в логе одно предупреждение; после появления каталога реле создаёт подкаталоги и обрабатывает запросы #### Scenario: Сбой протокола своего HTTP - **WHEN** при пересылке запроса свой HTTP оборвал ответ (`http.client.HTTPException`) - **THEN** в `resp/` ответ со `status = 502` и текстом ошибки; реле продолжает работу #### Scenario: Повторная подготовка не трогает взятые запросы - **WHEN** реле работает, в `work/` лежит взятый запрос, и пропал подкаталог `resp/` - **THEN** реле создаёт `resp/` заново; файл в `work/` не удалён, в логе нет «удалён незавершённый запрос»