333 lines
34 KiB
Markdown
333 lines
34 KiB
Markdown
# jira-hub
|
||
|
||
Хаб моста Jira для АРМ КА: зеркало задач проектов-меток 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`.
|
||
|
||
Код: ядро — `src/jira_hub/core/`, модуль jira — `src/jira_hub/modules/jira/`, веб — `web/src/` (модуль jira — `web/src/modules/jira/`); карта — `ARCHITECTURE.md`.
|
||
|
||
Данные лежат в `~/.local/share/jira-hub/` (каталог переопределяется переменной `JIRA_HUB_HOME`):
|
||
`hub.sqlite`, `backups/`, `release/` и `files/` (файлы вложений по sha256; каталоги — 0700). Пакет `mcp` закреплён версией `<2` (рулинг R-10): MCP SDK 2.x ломает API.
|
||
|
||
## Первый запуск
|
||
|
||
```bash
|
||
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
|
||
|
||
```bash
|
||
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 (по умолчанию `jira`); он ходит в API хаба клиентским токеном. Если хаб не запущен, инструменты
|
||
отвечают «хаб не запущен: jira-hub serve». Регистрация вручную (файл `~/.claude.json` скрипты хаба не правят),
|
||
в user scope — MCP нужен и в `~/projects/jira-bridge`, и в `~/projects/awp-ca`, где ведут задачи AURORA:
|
||
|
||
```bash
|
||
claude mcp add -s user jira-hub -- uv run --project /Users/andrey.epifancev/projects/jira-bridge/jira-hub jira-hub mcp
|
||
```
|
||
|
||
После перезапуска хаба или обновления кода MCP — `/mcp` → reconnect.
|
||
|
||
Сессия MCP работает в разрезе одного проекта-метки. Привязка — `--project <метка>` в `.mcp.json` репозитория проекта
|
||
(метка сравнивается точно, с учётом регистра):
|
||
|
||
```json
|
||
{"mcpServers": {"jira-hub": {"command": "uv", "args": ["run", "--project", "/Users/andrey.epifancev/projects/jira-bridge/jira-hub", "jira-hub", "mcp", "--project", "AWPCA"]}}}
|
||
```
|
||
|
||
Без привязки (user scope, без `--project`) работают только служебные инструменты (`projects`, `project_create`,
|
||
`agent_status`, `sync_log`, `settings_get`, `settings_set`, `batches`, `batch_status`, `batch_wait`, `batch_cancel`);
|
||
`issues`, `issue`, `comment`, `notes_set`, `batch_create`, `command_retry` и `refresh`
|
||
отвечают «сессия не привязана к проекту: запустите MCP с --project <метка> (список — projects)». Метка, которой нет в
|
||
хабе, — «проект <метка> не заведён в хабе: project_create или исправьте --project в .mcp.json». В привязанной сессии
|
||
`issues`, `batches` и `notes_set` видят только свой проект (`notes_set` — свои задачи и временные ключи `new:<n>`),
|
||
`issue` и `comment` открывают любую задачу по ключу, пакеты и команды ставятся с проектом: хаб сам дописывает метку проекта в
|
||
`ensure_issue` и отвергает чужую метку.
|
||
|
||
| Инструмент | Параметры | Результат |
|
||
|---|---|---|
|
||
| `issues` | фильтры как у `GET /issues`, `limit` | таблица задач проекта сессии |
|
||
| `issue` | `key` | поля, связи, заметки, последние команды, 5 последних комментариев (номер «#<id>», текст до 300 символов), 5 последних изменений («изменения (N)») |
|
||
| `comment` | `key`, `comment_id` (номер из `issue`) | один комментарий целиком: автор, время, полный текст |
|
||
| `changes` | `hours`, `since`, `until` (ISO; без смещения — местное время), `kinds` (`status`, `description`, `dates`, `comment`, `other` или `all`), `key` | лента изменений по задачам: что закрылось, правки описания, сроки, новые комментарии; по умолчанию — с начала суток; без привязки — только с `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 пакета или итог пакета |
|
||
| `projects` | нет | проекты хаба: метка, название, доска, тип доски, число задач; проект сессии отмечен |
|
||
| `project_create` | `label`, `name`, `board_id` (необязательно) | проект заведён, пакет обновления `#N`, готовый фрагмент `.mcp.json` для репозитория проекта |
|
||
| `agent_status` | нет | онлайн/офлайн, версия, текущая команда, 429 за сутки |
|
||
| `sync_log` | `limit` | последние загрузки состояния и «журнал: принят / нет» |
|
||
| `settings_get`, `settings_set` | ключ, значение | значение / новая `settings_version` |
|
||
|
||
Автор команд из MCP: `claude`; из API без автора: `api`; из веб-приложения: `viewer`.
|
||
|
||
В примере у `ensure_issue` нет `labels`: в привязанной сессии метку проекта дописывает хаб. Новый проект — `project_create`:
|
||
в ответе «проект <метка> «<название>» заведён, пакет обновления #N», затем указание положить в корень репозитория проекта
|
||
`.mcp.json` (слить с имеющимся) и переподключить MCP (`/mcp` → reconnect, одобрить сервер проекта) и сам фрагмент `.mcp.json`
|
||
с командой `uv run --project <корень хаба> jira-hub mcp --project <метка>` (модуль по умолчанию `jira`; явно — `jira-hub mcp jira --project <метка>`).
|
||
|
||
«Ошибка» с файлом одним пакетом (нужна настройка `issue_types.bug`, см. «Контракт команд»): MCP читает файл по `path`,
|
||
загружает его `POST /files` и ставит `ensure_attachment` с `file` и `filename` (по умолчанию — имя файла). Нет файла,
|
||
нет прав, не файл, больше 10 МБ — ответ с причиной и путём, пакет не ставится.
|
||
|
||
```json
|
||
[{"type": "ensure_issue", "params": {"temp_key": "new:1", "issue_type": "bug", "summary": "ОСВ 60 не распознана"}},
|
||
{"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` и включается настройкой:
|
||
|
||
```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> …»; исправление публикуется следующим номером.
|
||
Снять публикацию нельзя: откат «назад» — тот же старый код с новым номером.
|
||
|
||
Если записать публикацию не удалось (диск полон, нет прав), команда печатает «не удалось записать публикацию: …» и
|
||
выходит с кодом 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` | агент | загрузка состояния задач пачками |
|
||
| `POST /agent/history` | агент | журнал изменений Jira пачками (агент v13+, по `history` в `refresh`) → `{accepted, duplicates}`; повторы отбрасываются, неверная пачка — 422 `bad_history` |
|
||
| `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 /projects`, `POST /projects` | клиент | проекты `{projects: […]}` (метка, название, `board_id`, `board_type`, число задач) / завести `{label, name, board_id?, author?}` → 201 `{project, batch_id}` и пакет «Новый проект <метка>» (`refresh` по общей JQL); 409 `project_exists`, 422 `bad_project`, 422 `bad_batch` (постановка `refresh` отказана) |
|
||
| `GET /issues` | клиент | фильтры `project` (метка; неизвестная — 404), `sprint`, `assignee`, `work_kind`, `fact_status`, `status`, `label`, `epic`, `neighbor`, `q`, `hide_closed` (`1` — без задач в статусах из настройки `closed_statuses`, по умолчанию «Готово», «Снято»), `limit`, `offset` |
|
||
| `GET /issues/{key}` | клиент | задача целиком, с комментариями и журналом изменений `changes` (новые сверху) |
|
||
| `GET /changes` | клиент | лента изменений: `since`, `until` (ISO со смещением или `Z`, иначе 422; по умолчанию — последние 24 ч), `project` (только свои задачи), `key`, `kinds`, `limit` (до 2000) → `{since, until, truncated, changes}` |
|
||
| `GET /board` | клиент | `?project=<метка>` (неизвестная — 404): снимок для доски — свои задачи проекта, соседи (связи в обе стороны) и эпики своих задач, с `fact_status`/`work_kind`; связи — те, у которых хотя бы один конец — своя задача проекта (без `project` — все задачи и связи), `last_sync` — последний успешный полный прогон |
|
||
| `GET /`, `GET /links` | — | перенаправление на `/app/board` |
|
||
| `PUT /issues/{key}/note` | клиент | заметки (частично) |
|
||
| `POST /batches`, `GET /batches`, `GET /batches/{id}` | клиент | пакеты команд; `project` в теле `POST /batches` — пакет проекта (проверки метки, ограничение задач проектом), `?project=` у `GET /batches` — только пакеты проекта |
|
||
| `POST /batches/{id}/cancel` | клиент | отмена пакета |
|
||
| `POST /commands/{id}/retry`, `POST /commands/{id}/cancel` | клиент | повтор (новый пакет; `project` в теле — отказ, если команда не из этого проекта) / отмена (`force` для взятой) |
|
||
| `POST /refresh` | клиент | `{keys}`, `{jql}` или `{full: true}` (общая JQL по меткам всех проектов; проектов нет — 422 «нет проектов») |
|
||
| `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`), журнал загрузок (`history_at` — когда принят журнал изменений полного прогона) |
|
||
| `GET /settings`, `PUT /settings` | клиент | настройки (PUT частично, поднимает `settings_version`; ответ — все настройки после записи, как у GET); `base_jql` вычисляется из проектов (без проектов — `null`), запись `base_jql` и `board_id` — 422 |
|
||
|
||
```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?project=<метка>`, задачи `/app/issues?project=<метка>`, лента изменений `/app/feed?project=<метка>` (период и виды событий, история — и в карточке задачи), карточка `/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
|
||
```
|
||
|
||
Проект выбирается выпадающим списком «Проект» в верхней панели (метка и название); пункт «+ Новый проект…» открывает окно
|
||
(метка, название, необязательно номер доски Jira): после создания приложение переходит в новый проект и показывает ход
|
||
пакета обновления. Текущий проект — параметр `project` в адресе; без него приложение подставляет последний выбранный
|
||
(он помнится в браузере) или первый из списка; метка, которой нет в хабе, — «проект <метка> не найден». Раскладка и
|
||
флаги доски хранятся отдельно для каждого проекта (у AWPCA — прежние ключи `localStorage`, у остальных с суффиксом `@<метка>`).
|
||
|
||
Выкатка проектов (порядок обязателен): агент v12 опубликован и `agent_status` показывает v12 → слияние хаба, `make web`,
|
||
перезапуск хаба (`launchctl kickstart`, только владелец; миграция схемы v4 заводит проект AWPCA «АРМ КА») →
|
||
`min_agent_version = 12` → в репозиторий `awp-ca` положить `.mcp.json` с `--project AWPCA` и переподключить MCP. Хаб
|
||
новее агента v11 не выкатывать до первого шага: v11 не знает доски проекта в параметрах команд. Доску проекта
|
||
исправляет только владелец, прямо в базе хаба: `UPDATE project SET board_id = <номер>, board_type = NULL WHERE label = '<метка>'`
|
||
(тип доски определит следующий `refresh`).
|
||
|
||
После перезапуска хаба: нажать «Обновить из Jira», затем `projects` в MCP должен показать у AWPCA тип доски `scrum`
|
||
(до этого `ensure_sprint` AWPCA отвечает 422 «тип доски ещё не известен»). Между `git pull` и перезапуском хаба не
|
||
открывать новые сессии MCP: новый MCP/CLI мигрирует базу под работающим старым хабом (миграция v4 ему не вредит, но
|
||
лучше без неё).
|
||
|
||
Без `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 из настройки `closed_statuses`, по умолчанию «Готово», «Снято»):
|
||
флажок «скрыть закрытые», снятый — `closed=1` в адресе; на доске закрытые задачи и их связи приглушены.
|
||
|
||
Карточка задачи: в шапке — статус 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`); доску проекта задачи (`board_id`) дописывает хаб, от клиента параметр отвергается |
|
||
| `ensure_attachment` | `key` (ключ или `new:<n>`), `file` (sha256 из `POST /files`), `filename` (имя вложения в Jira, без `/`, `\`, `"` и управляющих символов); `size` хаб дописывает при выдаче агенту |
|
||
| `refresh` | `keys` или `jql`; при выдаче агенту хаб дописывает `boards` — доски проектов |
|
||
|
||
Тип `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`):
|
||
|
||
```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`, `ensure_attachment` 30 с, `refresh` 2 с (настройка `pacing`).
|
||
|
||
`ensure_attachment` ставится, только если файл уже загружен (`POST /files`), иначе 422 «команда N: файл <sha> не
|
||
загружен: 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
|
||
<?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`.
|
||
|
||
## Тесты
|
||
|
||
```bash
|
||
uv run pytest
|
||
```
|