# jira-hub Хаб моста Jira для АРМ КА: зеркало задач проекта AURORA (метка AWPCA) в SQLite, служебные заметки, очередь команд, HTTP API в локальной сети и MCP-сервер для Claude. Хаб работает на Mac mini (`192.168.0.106`); в Jira ходит только агент на виртуалке (репозиторий `jira-agent`), пароль Jira хабу неизвестен. Спека: `openspec/changes/bridge-core/` (proposal, design, specs). ## Установка ```bash uv sync ``` Данные лежат в `~/.local/share/jira-hub/` (каталог переопределяется переменной `JIRA_HUB_HOME`): `hub.sqlite` и `backups/`. Пакет `mcp` закреплён версией `<2` (рулинг R-10): MCP SDK 2.x ломает API. ## Первый запуск ```bash uv run jira-hub init --config ../admin/jira-sync/config.example.json uv run jira-hub serve # 0.0.0.0:8790 ``` `init` переносит в настройки хаба всё, что относится к проекту Jira (поля, типы, карту переходов, типы связей, доску, приоритеты), и создаёт два токена: агентский и клиентский. Адрес и учётные данные Jira (`base_url`, `auth`, `verify_ssl`, `ca_bundle`) остаются у агента и в хаб не попадают. Повторный `init` отказывает ("уже инициализирован"); с `--force` настройки переписываются. Токены можно показать позже: `uv run jira-hub tokens`. Передать агенту нужно адрес хаба и агентский токен: через общую папку виртуалки положить в `agent.json` `"hub_url": "http://192.168.0.106:8790"` и `"hub_token": "<агентский токен>"`. Клиентский токен остаётся на этой машине: его использует MCP (читает из базы сам). Порт меняется ключом `--port` или настройкой `port`; адрес прослушивания — `--host`. ## Импорт пакета admin/jira-sync ```bash uv run jira-hub import ../admin/jira-sync ``` Переносит из CSV заметки (статус по факту, вид работы и т. п.) и выводит отчёт JSON: `notes` (сколько заметок записано), `expected_links`, `missing_keys` (ключи из CSV, которых нет в зеркале) и `missing_links` (ожидаемые связи, которых нет). Запускать после первого `refresh` с `full: true`: пока зеркало пусто, все ключи будут в `missing_keys`. Заметки к ещё не загруженным задачам сохраняются. ## MCP в Claude `jira-hub mcp` запускает MCP-сервер по stdio; он ходит в API хаба клиентским токеном. Если хаб не запущен, инструменты отвечают «хаб не запущен: jira-hub serve». Регистрация вручную (файл `~/.claude.json` скрипты хаба не правят), блок для проекта `awp-ca`: ```json "jira-hub": {"type": "stdio", "command": "uv", "args": ["run", "--project", "/Users/andrey.epifancev/projects/awp-ca/jira-hub", "jira-hub", "mcp"]} ``` Либо командой: `claude mcp add jira-hub -- uv run --project /Users/andrey.epifancev/projects/awp-ca/jira-hub jira-hub mcp`. | Инструмент | Параметры | Результат | |---|---|---| | `issues` | фильтры как у `GET /issues`, `limit` | таблица задач | | `issue` | `key` | поля, связи, заметки, последние команды | | `notes_set` | `key`, поля заметок | обновлённые заметки | | `batch_create` | `title`, `commands` | `id`, оценка длительности, число команд | | `batch_status` | `id` | статусы команд, ошибки, оценка остатка | | `batch_wait` | `id`, `timeout_minutes` (до 60) | как `batch_status`, по завершении или таймауту; ожидание асинхронное, сервер не блокируется | | `batches` | `limit` | последние пакеты | | `command_retry`, `batch_cancel` | `id` | новый пакет / результат отмены | | `refresh` | `keys` или `full`, `wait` (по умолчанию `false`: только ставит пакет; `true` — ждёт до 30 мин) | id пакета или итог пакета | | `agent_status` | нет | онлайн/офлайн, версия, текущая команда, 429 за сутки | | `sync_log` | `limit` | последние загрузки состояния | | `settings_get`, `settings_set` | ключ, значение | значение / новая `settings_version` | Автор команд из MCP: `claude`; из API без автора: `api`. ## Транспорт через общую папку Если сеть между виртуалкой и Mac mini закрыта (VPN «Континент ZTN» на виртуалке режет локальную сеть), агент ходит в хаб через общую папку UTM. Реле работает внутри `serve` и включается настройкой: ```bash TOKEN=$(uv run jira-hub tokens | sed -n 's/^клиентский токен: //p') curl -s -X PUT -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \ --data '{"relay_dir": "/Volumes/externalSSD/utm/rshb/jira-bridge"}' http://127.0.0.1:8790/settings ``` или из Claude: `settings_set relay_dir "/Volumes/externalSSD/utm/rshb/jira-bridge"`. Выключить — `null`. Перезапуск не нужен: реле перечитывает настройку раз в 5 с, создаёт `req/`, `resp/`, `work/`; если папки нет (SSD не подключён) — пишет одно предупреждение и ждёт. У агента: `"hub_url": "folder:/Volumes/My Shared Files/RSHB/jira-bridge"`. Как устроено: агент пишет запрос файлом `req/.json` (`method`, `path`, `body`, токен, `deadline`), реле переименованием забирает его в `work/`, пересылает в свой HTTP и кладёт ответ `resp/.json` (`status`, `body`). Просроченные запросы выбрасываются; `claim` пересылается, только если до срока ≥ 35 с. В логе — строка `relay <метод> <путь> <код> <секунды>` на запрос. Круг через VirtioFS ≈ 0,8 с. Если launchd-хаб не видит папку (в логе «relay: нет доступа к папке … — Operation not permitted; жду» или «папки … нет — жду» при подключённом SSD) — macOS не пускает процесс на внешний диск: «Системные настройки → Конфиденциальность и безопасность → Полный доступ к диску» → добавить `/opt/homebrew/bin/uv` и python из `.venv` хаба, перезапустить задание. ## HTTP API Все маршруты требуют `Authorization: Bearer <токен>`; ответы JSON, ошибки `{error, detail}` (400/401/404/409/422). | Маршрут | Токен | Назначение | |---|---|---| | `POST /agent/claim` | агент | взять следующую команду (долгий запрос до 30 с) | | `GET /agent/settings` | агент | настройки для агента (без токенов) | | `GET /agent/commands/{id}` | агент | команда по id | | `POST /agent/commands/{id}/result` | агент | отчёт о выполнении (идемпотентно) | | `POST /agent/state` | агент | загрузка состояния задач пачками | | `GET /agent/known-keys` | агент | ключи зеркала | | `GET /issues` | клиент | фильтры `sprint`, `assignee`, `work_kind`, `fact_status`, `status`, `label`, `epic`, `neighbor`, `q`, `limit`, `offset` | | `GET /issues/{key}` | клиент | задача целиком | | `PUT /issues/{key}/note` | клиент | заметки (частично) | | `POST /batches`, `GET /batches`, `GET /batches/{id}` | клиент | пакеты команд | | `POST /batches/{id}/cancel` | клиент | отмена пакета | | `POST /commands/{id}/retry`, `POST /commands/{id}/cancel` | клиент | повтор (новый пакет) / отмена (`force` для взятой) | | `POST /refresh` | клиент | `{keys}`, `{jql}` или `{full: true}` | | `GET /agent`, `GET /sync-runs` | клиент | состояние агента, журнал загрузок | | `GET /settings`, `PUT /settings` | клиент | настройки (PUT частично, поднимает `settings_version`) | ```bash TOKEN=$(uv run jira-hub tokens | sed -n 's/^клиентский токен: //p') curl -s -H "Authorization: Bearer $TOKEN" "http://127.0.0.1:8790/issues?sprint=Спринт%202&limit=20" ``` ## Контракт команд Каждая команда описывает желаемое состояние одной задачи; агент читает, сравнивает и пишет только разницу, повтор с тем же результатом завершается `done` без записи. | Тип | Параметры | |---|---| | `ensure_issue` | `temp_key` (`new:`), `issue_type` (`task`/`epic`), `summary`, `description`, `priority`, `duedate`, `planned_start`, `planned_end`, `labels`, `epic_key`, `assignee` | | `ensure_fields` | `key` и подмножество: `summary`, `description`, `priority`, `duedate`, `planned_start`, `planned_end`, `labels_add`, `labels_remove`, `epic_key`, `assignee` | | `ensure_comment` | `key`, `marker`, `body` | | `ensure_status` | `key`, `target` (имя статуса или список-путь) | | `ensure_link` | `source`, `target`, `link_type`, `replace` | | `ensure_sprint` | `key`, `sprint` (имя или `backlog`) | | `refresh` | `keys` или `jql` | Ключ в параметре может быть временным `new:`: команда выдаётся после `ensure_issue` с тем же `temp_key` из того же пакета. В хвост пакета хаб сам добавляет `refresh` по затронутым ключам. Пример пакета (`POST /batches`): ```json {"title": "1538 в спринт 2", "author": "claude", "commands": [ {"type": "ensure_fields", "params": {"key": "AURORA-1538", "duedate": "2026-10-23"}}, {"type": "ensure_link", "params": {"source": "AURORA-1537", "target": "AURORA-1538", "link_type": "связанные", "replace": true}}, {"type": "ensure_sprint", "params": {"key": "AURORA-1538", "sprint": "Спринт 2"}} ]} ``` Ответ: `{id, estimate_seconds}`. Зазоры между записями в Jira по умолчанию: `ensure_issue` и `ensure_fields` 120 с, `ensure_comment`, `ensure_status`, `ensure_link`, `ensure_sprint` 30 с, `refresh` 2 с (настройка `pacing`). ## Пример сценария 1. `jira-hub serve` на Mac mini, агент запущен на виртуалке. 2. В Claude: `refresh` с `full` загружает задачи в зеркало; `jira-hub import` переносит заметки из CSV. 3. `batch_create` ставит пакет; `batch_wait` ждёт результата; `issue` показывает итог. ## Эксплуатация - Резервные копии: перед первой записывающей командой пакета хаб копирует базу в `backups/<дата-время>-.sqlite` и хранит последние `backup_keep` копий (по умолчанию 30). - Автообновление: если `auto_refresh_minutes > 0` (по умолчанию 60), агент на связи и очередь пуста дольше интервала, хаб ставит пакет «Автообновление». - Настройки: `PUT /settings` или инструмент `settings_set`; защищены `settings_version`, `schema_version`, `tokens`. - Команда «нет ответа от агента»: команда в `claimed` без отчёта. Дождитесь агента (перезапущенный агент отчитается) или отмените: `POST /commands/{id}/cancel` с `{"force": true}`. Хаб сам команды не повторяет; если факт уже виден после `refresh`, команда становится `confirmed_by_state`. Повтор: `command_retry`. - Автозапуск `serve` через launchd: `~/Library/LaunchAgents/ru.aepif.jira-hub.plist` ```xml Labelru.aepif.jira-hub ProgramArguments uvrun--project /Users/andrey.epifancev/projects/awp-ca/jira-hub jira-hubserve EnvironmentVariables PATH/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin RunAtLoad KeepAlive StandardErrorPath/tmp/jira-hub.err.log ``` Загрузить: `launchctl load ~/Library/LaunchAgents/ru.aepif.jira-hub.plist`. ## Тесты ```bash uv run pytest ```