2026-10-17 11:41:47 +04:00

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

Python для хаба uv ставит свой (3.13, .python-version; системный и conda-Python не берутся — tool.uv в pyproject.toml): в SQLite 3.51.0–3.51.1 есть взаимная блокировка, от которой хаб зависал под параллельными запросами. Первый uv sync или uv run после смены сам пересоздаёт .venv.

Данные лежат в ~/.local/share/jira-hub/ (каталог переопределяется переменной JIRA_HUB_HOME): hub.sqlite, backups/, release/ и files/ (файлы вложений по sha256; каталоги — 0700). Пакет mcp закреплён версией <2 (рулинг R-10): MCP SDK 2.x ломает API.

Первый запуск

uv run jira-hub init --config ~/projects/awp-ca/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 ~/projects/awp-ca/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 скрипты хаба не правят), в user scope — MCP нужен и в ~/projects/jira-bridge, и в ~/projects/awp-ca, где ведут задачи AURORA:

claude mcp add -s user jira-hub -- uv run --project /Users/andrey.epifancev/projects/jira-bridge/jira-hub jira-hub mcp

После перезапуска хаба или обновления кода MCP — /mcp → reconnect.

Инструмент Параметры Результат
issues фильтры как у GET /issues, limit таблица задач
issue key поля, связи, заметки, последние команды
notes_set key, поля заметок обновлённые заметки
batch_create title, commands (у ensure_attachment вместо file можно path — абсолютный путь к локальному файлу до 10 МБ) 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.

«Ошибка» с файлом одним пакетом (нужна настройка issue_types.bug, см. «Контракт команд»): MCP читает файл по path, загружает его POST /files и ставит ensure_attachment с file и filename (по умолчанию — имя файла). Нет файла, нет прав, не файл, больше 10 МБ — ответ с причиной и путём, пакет не ставится.

[{"type": "ensure_issue", "params": {"temp_key": "new:1", "issue_type": "bug", "summary": "ОСВ 60 не распознана", "labels": ["AWPCA"]}},
 {"type": "ensure_sprint", "params": {"key": "new:1", "sprint": "Спринт 2"}},
 {"type": "ensure_attachment", "params": {"key": "new:1", "path": "/Users/…/ОСВ сч. 60 за 2 кв. 2026.xlsx"}}]

Транспорт через общую папку

Если сеть между виртуалкой и 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 хаба, перезапустить задание.

Самообновление агента

Агент v5+ под запускателем run.py обновляется сам (спека openspec/specs/agent/, «Самообновление агента»). Опубликовать новую версию:

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/413/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 /agent/files/{sha256} агент файл вложения {file, size, content} (content — base64); 404 no_file
GET /issues клиент фильтры sprint, assignee, work_kind, fact_status, status, label, epic, neighbor, q, hide_closed (1 — без задач в статусах из настройки closed_statuses, по умолчанию «Готово», «Снято»), 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}
POST /files клиент загрузить файл вложения {content: base64} → {file: sha256, size}; не base64 — 400, пустой — 422 empty_file, больше 10 МБ (10 485 760 байт) — 413 too_large; повтор того же содержимого — тот же file
GET /agent, GET /sync-runs клиент состояние агента (с release, update_state, launcher_version, agent_reported, update_hint), журнал загрузок
GET /settings, PUT /settings клиент настройки (PUT частично, поднимает settings_version; ответ — все настройки после записи, как у GET)
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} — окно поверх списка или доски (закрытие — ✕, Esc, щелчок по фону; прямой адрес открывает окно поверх списка задач); / и /links перенаправляют на доску. Вход по клиентскому токену (uv run jira-hub tokens), токен хранится в браузере; localhost и 127.0.0.1 — разные сайты для браузера, токен и раскладка у каждого свои. Выкатка на Mac mini:

cd ~/projects/jira-bridge/jira-hub
git pull
make web
launchctl kickstart -k gui/$(id -u)/ru.aepif.jira-hub

Без make web хаб на /app… отвечает 503 с подсказкой. Шрифты (IBM Plex) входят в сборку: приложение работает без доступа в интернет. Раскладка доски и вход сохраняются: приложение читает те же ключи localStorage, что прежняя страница. Разработка: bash web/scripts/test-hub.sh (тестовый хаб :8791 с синтетическими данными) и cd web && npm run dev (http://127.0.0.1:5173/app/).

Карточка задачи: в шапке — статус Jira, статус по факту, исполнитель, срок (просроченный — красным), спринт и эпик; связи сгруппированы «Зависит от» / «Блокирует» / «Связана с» / «Прочие» с темой и статусом связанной задачи; комментарии из Jira (агент v10+) новые сверху, «Скрыть» запоминается в браузере; заметки — режим чтения, «Править» открывает форму. Комментарии появляются после первого обновления из Jira агентом v10.

Контракт команд

Каждая команда описывает желаемое состояние одной задачи; агент читает, сравнивает и пишет только разницу, повтор с тем же результатом завершается done без записи.

Тип Параметры
ensure_issue temp_key (new:<n>), issue_type (task/epic/bug), 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)
ensure_attachment key (ключ или new:<n>), file (sha256 из POST /files), filename (имя вложения в Jira, без /, \, " и управляющих символов); size хаб дописывает при выдаче агенту
refresh keys или jql

Тип bug — «Ошибка» в Jira, обязательные поля те же, что у задачи. Ставится, только если в настройках есть issue_types.bug с непустым name или id, иначе 422 «команда N: нет настройки issue_types.bug». init берёт его из ключей конфига issue_type_bug/issue_type_bug_id, если они есть; на живом хабе — settings_set issue_types с объектом целиком: {"task": {…}, "epic": {…}, "bug": {"name": "Ошибка", "id": null}}.

Ключ в параметре может быть временным 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, ensure_attachment 30 с, refresh 2 с (настройка pacing).

ensure_attachment ставится, только если файл уже загружен (POST /files), иначе 422 «команда N: файл не загружен: POST /files». Агент ищет у задачи вложение с тем же именем и размером; нет — скачивает файл у хаба (GET /agent/files/{sha256}) и прикладывает. По зеркалу команда не подтверждается (вложений в зеркале нет).

Пример сценария

  1. jira-hub serve на Mac mini, агент запущен на виртуалке.
  2. В Claude: refresh с full загружает задачи в зеркало; jira-hub import переносит заметки из CSV.
  3. batch_create ставит пакет; batch_wait ждёт результата; issue показывает итог.

Эксплуатация

  • Резервные копии: перед первой записывающей командой пакета хаб копирует базу в backups/<дата-время>-<batch>.sqlite и хранит последние backup_keep копий (по умолчанию 30).
  • Файлы вложений: files/<sha256>, одна копия на содержимое, в резервные копии базы не входят. Раз в сутки (первый раз — через минуту после старта) хаб удаляет файлы старше 30 дней, на которые не ссылается ensure_attachment в очереди или у агента; повтор такой команды после уборки — 422 «файл … не загружен: POST /files», файл загружается заново.
  • Автообновление: если 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/jira-bridge/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
Description
No description provided
Readme 9.9 MiB
Languages
TypeScript 48.1%
Python 47.9%
CSS 3.9%