M-3 (hub#4, PR #2). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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).
Установка
uv sync
Данные лежат в ~/.local/share/jira-hub/ (каталог переопределяется переменной JIRA_HUB_HOME):
hub.sqlite и backups/. Пакет mcp закреплён версией <2 (рулинг R-10): MCP SDK 2.x ломает API.
Первый запуск
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
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:
"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 и включается настройкой:
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/<id>.json (method, path, body, токен, deadline), реле
переименованием забирает его в work/, пересылает в свой HTTP и кладёт ответ resp/<id>.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) |
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:<n>), 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:<n>: команда выдаётся после ensure_issue с тем же temp_key из того же
пакета. В хвост пакета хаб сам добавляет refresh по затронутым ключам. Пример пакета (POST /batches):
{"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).
Пример сценария
jira-hub serveна Mac mini, агент запущен на виртуалке.- В Claude:
refreshсfullзагружает задачи в зеркало;jira-hub importпереносит заметки из CSV. batch_createставит пакет;batch_waitждёт результата;issueпоказывает итог.
Эксплуатация
- Резервные копии: перед первой записывающей командой пакета хаб копирует базу в
backups/<дата-время>-<batch>.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 version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"><dict>
<key>Label</key><string>ru.aepif.jira-hub</string>
<key>ProgramArguments</key><array>
<string>uv</string><string>run</string><string>--project</string>
<string>/Users/andrey.epifancev/projects/awp-ca/jira-hub</string>
<string>jira-hub</string><string>serve</string>
</array>
<key>EnvironmentVariables</key><dict>
<key>PATH</key><string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
</dict>
<key>RunAtLoad</key><true/>
<key>KeepAlive</key><true/>
<key>StandardErrorPath</key><string>/tmp/jira-hub.err.log</string>
</dict></plist>
Загрузить: launchctl load ~/Library/LaunchAgents/ru.aepif.jira-hub.plist.
Тесты
uv run pytest