# 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` (ожидаемые связи, которых нет), `reversed_links` (ожидаемые связи, которые в Jira стоят в обратную сторону — для «Связанных» это та же связь). Запускать после первого `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` хаба, перезапустить задание. ## Самообновление агента Агент v5+ под запускателем `run.py` обновляется сам (спека `openspec/specs/agent/`, «Самообновление агента»). Опубликовать новую версию: ```bash uv run jira-hub agent publish ../jira-agent/src/agent.py ``` Версия берётся из строки `AGENT_VERSION = N` и должна быть больше опубликованной; файл и `manifest.json` ложатся в `~/.local/share/jira-hub/release/` (0700). Перезапуск хаба не нужен: версия уходит агенту в ответе `claim` (`agent_release`), файл — по `GET /agent/release`. Агент ставит её в паузе между командами; через несколько минут `agent_status` (MCP) покажет «обновление: ok v<старая> → v<новая>». Если новая версия упала или не вышла на связь, запускатель вернёт прежнюю — `agent_status` покажет «откат с v …»; исправление публикуется следующим номером. Снять публикацию нельзя: откат «назад» — тот же старый код с новым номером. Если записать публикацию не удалось (диск полон, нет прав), команда печатает «не удалось записать публикацию: …» и выходит с кодом 1; опубликованной остаётся прежняя версия, временных файлов в `release/` нет. Манифест, который хаб не может разобрать (`version` не целое, `sha256` не 64 символа `[0-9a-f]`), означает «публикации нет» — одно предупреждение в логе хаба на каждую новую ошибку. Состояние обновления и версию запускателя агент сообщает в каждом `claim`, хаб держит их в памяти: сразу после перезапуска хаба, пока агент не вышел на связь (до первого `claim` агента, обычно до 30 с), `agent_status` пишет «запускатель: неизвестно (агент ещё не выходил на связь после перезапуска хаба)», а подсказки «агент запущен без run.py» нет. ## HTTP API Все маршруты, кроме веб-приложения (`GET /app…`) и перенаправления старых адресов (`GET /`, `GET /links`), требуют `Authorization: Bearer <токен>`; ответы JSON, ошибки `{error, detail}` (400/401/404/409/422). | Маршрут | Токен | Назначение | |---|---|---| | `POST /agent/claim` | агент | взять следующую команду (долгий запрос до 30 с); в теле — `update_state`, `launcher_version`; в ответе — `agent_release` (опубликованная версия агента или `null`) | | `GET /agent/settings` | агент | настройки для агента (без токенов) | | `GET /agent/commands/{id}` | агент | команда по id | | `POST /agent/commands/{id}/result` | агент | отчёт о выполнении (идемпотентно) | | `POST /agent/state` | агент | загрузка состояния задач пачками | | `GET /agent/known-keys` | агент | ключи зеркала | | `GET /agent/release` | агент | опубликованная версия агента `{version, sha256, content}`; 404 `no_release` | | `GET /issues` | клиент | фильтры `sprint`, `assignee`, `work_kind`, `fact_status`, `status`, `label`, `epic`, `neighbor`, `q`, `limit`, `offset` | | `GET /issues/{key}` | клиент | задача целиком | | `GET /board` | клиент | снимок для доски: все задачи с `fact_status`/`work_kind`, все связи, `last_sync` — последний успешный полный прогон | | `GET /`, `GET /links` | — | перенаправление на `/app/board` | | `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` | клиент | состояние агента (с `release`, `update_state`, `launcher_version`, `agent_reported`, `update_hint`), журнал загрузок | | `GET /settings`, `PUT /settings` | клиент | настройки (PUT частично, поднимает `settings_version`; ответ — все настройки после записи, как у GET) | ```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" ``` ## Веб-приложение Адреса: `http://127.0.0.1:8790/app/` — доска `/app/board`, задачи `/app/issues`, карточка `/app/issues/{key}`; `/` и `/links` перенаправляют на доску. Вход по клиентскому токену (`uv run jira-hub tokens`), токен хранится в браузере; `localhost` и `127.0.0.1` — разные сайты для браузера, токен и раскладка у каждого свои. Выкатка на Mac mini: ``` cd ~/projects/awp-ca/jira-hub git pull make web launchctl kickstart -k gui/$(id -u)/ru.aepif.jira-hub ``` Без `make web` хаб на `/app…` отвечает 503 с подсказкой. Раскладка доски и вход сохраняются: приложение читает те же ключи `localStorage`, что прежняя страница. Разработка: `bash web/scripts/test-hub.sh` (тестовый хаб :8791 с синтетическими данными) и `cd web && npm run dev` (http://127.0.0.1:5173/app/). ## Контракт команд Каждая команда описывает желаемое состояние одной задачи; агент читает, сравнивает и пишет только разницу, повтор с тем же результатом завершается `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`. - Довыдача: если агент пришёл за новой командой, а выданная ему осталась без отчёта (ответ потерян, агент перезапущен), хаб выдаёт её снова; `agent_status` показывает такие команды с числом выдач. После 3 выдач без отчёта команда становится `failed` («агент не отчитался после 3 выдач»), очередь идёт дальше; повтор — `command_retry`. Отменить взятую команду: `POST /commands/{id}/cancel` с `{"force": true}`. - Автозапуск `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 ```