sdlc-hub/README.md
Andrey Epifancev 447d878e0e fix(hub): Python от uv вместо miniconda — SQLite 3.51.1 вешал хаб взаимной блокировкой (O-14, R-11, #29)
Стеки зависшего тестового хаба: потоки запросов ждут мьютексы unix-VFS SQLite в sqlite3.connect
(findReusableFd) и close (sqlite3WalClose -> unixLock). Ошибка SQLite 3.51.0–3.51.1, исправлена в 3.51.2.
.python-version = 3.13 и tool.uv.python-preference = only-managed: uv берёт свой CPython 3.13.12 (SQLite 3.50.4)
и сам пересоздаёт .venv. Тест не даёт вернуться на SQLite 3.51.0–3.51.1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 03:29:23 +04:00

242 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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
```
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/`. Пакет `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/<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/`, «Самообновление агента»).
Опубликовать новую версию:
```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<N> …»; исправление публикуется следующим номером.
Снять публикацию нельзя: откат «назад» — тот же старый код с новым номером.
## 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`, `update_hint`), журнал загрузок |
| `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"
```
## Веб-приложение
Адреса: `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 с подсказкой. Шрифты (IBM Plex) входят в сборку: приложение
работает без доступа в интернет. Раскладка доски и вход сохраняются: приложение
читает те же ключи `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:<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`):
```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/<дата-время>-<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
<?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`.
## Тесты
```bash
uv run pytest
```