Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
sdlc-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).
Установка
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/sdlc_hub/core/, модуль jira — src/sdlc_hub/modules/jira/, модуль wiki — src/sdlc_hub/modules/wiki/, веб — web/src/ (модуль jira — web/src/modules/jira/); карта — ARCHITECTURE.md.
Данные лежат в ~/.local/share/sdlc-hub/ (каталог переопределяется переменной SDLC_HUB_HOME):
hub.sqlite, backups/, release/<модуль>/, files/ (файлы вложений по sha256), downloads/ (копии вложений для MCP) и wiki/<метка>/ (Markdown-копия вики проекта); каталоги — 0700. Пакет mcp закреплён версией <2 (рулинг R-10): MCP SDK 2.x ломает API.
Первый запуск
uv run sdlc-hub init --config ~/projects/awp-ca/admin/jira-sync/config.example.json
uv run sdlc-hub serve # 0.0.0.0:8790
init переносит в настройки хаба всё, что относится к проекту Jira (поля, типы, карту переходов, типы связей, доску,
приоритеты), и создаёт токены: клиентский и по одному на агента каждого модуля (jira, wiki). Адрес и учётные данные Jira (base_url, auth, verify_ssl,
ca_bundle) остаются у агента и в хаб не попадают. Повторный init отказывает ("уже инициализирован"); с --force
настройки переписываются. Токены можно показать позже: uv run sdlc-hub tokens.
Передать агенту jira нужно адрес хаба и токен агента jira (uv run sdlc-hub tokens → «агент jira»): через общую папку виртуалки положить в agent.json
"hub_url": "http://192.168.0.106:8790" и "hub_token": "<токен агента jira>". Клиентский токен остаётся на этой машине:
его использует MCP (читает из базы сам).
Порт меняется ключом --port или настройкой port; адрес прослушивания — --host.
Импорт пакета admin/jira-sync
uv run sdlc-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
sdlc-hub mcp <модуль> запускает MCP-сервер модуля по stdio (модуль обязателен: jira или wiki; без него или с неизвестным — код 2); серверы называются sdlc-jira и sdlc-wiki; он ходит в API хаба клиентским токеном. Если хаб не запущен, инструменты
отвечают «хаб не запущен: sdlc-hub serve». Регистрация вручную (файл ~/.claude.json скрипты хаба не правят),
в user scope — MCP нужен и в ~/projects/jira-bridge, и в ~/projects/awp-ca, где ведут задачи AURORA:
claude mcp add -s user sdlc-jira -- uv run --project /Users/andrey.epifancev/projects/jira-bridge/sdlc-hub sdlc-hub mcp jira
После перезапуска хаба или обновления кода MCP — /mcp → reconnect.
MCP вики — отдельный сервер sdlc-wiki (sdlc-hub mcp wiki). Регистрация в .mcp.json репозитория проекта (рядом с sdlc-jira, с привязкой к проекту):
{
"mcpServers": {
"sdlc-wiki": {
"command": "uv",
"args": ["run", "--project", "/Users/andrey.epifancev/projects/jira-bridge/sdlc-hub", "sdlc-hub", "mcp", "wiki", "--project", "AWPCA"]
}
}
}
Инструменты: projects, project_create, root_set, tree, page, search, attachment, sync, status. Пока у проекта нет корня
вики (root_set DOC-… или wiki_root в project_create), они отвечают «у проекта <метка> нет корня вики».
Спринты и бэклог (нужна привязка к проекту; читают GET /agile проекта сессии): sprints — спринты доски с суммами
задач и SP «выполнено/всего»; sprint [name] — спринт (по умолчанию активный) с задачами по колонкам доски;
backlog [limit] — бэклог по рангу (по умолчанию 50 строк). В issues есть колонка SP. Команды спринтов и ранга ставятся
через batch_create, например смена состояния спринта с переносом открытых задач (хвост пакета хаб заменит на refresh по JQL проекта):
{"title": "Закрыть спринт 2", "commands": [
{"type": "ensure_sprint_meta", "params": {"sprint": "S2", "goal": "Цель", "start_date": "2026-10-14", "end_date": "2026-10-28"}},
{"type": "ensure_sprint_state", "params": {"sprint": "S2", "state": "closed", "move_open_to": "S3"}}]}
Сессия MCP работает в разрезе одного проекта-метки. Привязка — --project <метка> в .mcp.json репозитория проекта
(метка сравнивается точно, с учётом регистра):
{"mcpServers": {"sdlc-jira": {"command": "uv", "args": ["run", "--project", "/Users/andrey.epifancev/projects/jira-bridge/sdlc-hub", "sdlc-hub", "mcp", "jira", "--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 последних комментариев (номер «#», текст до 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 <корень хаба> sdlc-hub mcp jira --project <метка> (модуль указывается явно).
«Ошибка» с файлом одним пакетом (нужна настройка 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 не распознана"}},
{"type": "ensure_sprint", "params": {"key": "new:1", "sprint": "Спринт 2"}},
{"type": "ensure_attachment", "params": {"key": "new:1", "path": "/Users/…/ОСВ сч. 60 за 2 кв. 2026.xlsx"}}]
Вики и eva-agent
Копию вики Eva хаб получает от eva-agent — отдельного агента на той же виртуалке (отдельный репозиторий aep/eva-agent,
каталог jira-bridge/eva-agent/; Python 3.9, один файл src/agent.py + run.py — как jira-agent). Хаб в Eva не ходит и токен Eva не хранит.
uv run sdlc-hub tokens— взять токен агентаwiki(уjiraиwikiтокены разные; чужой токен на маршрутах другого агента даёт 401).- На виртуалке отдельным каталогом положить eva-agent и
agent.json:hub_url(folder:<путь>или http),hub_token— токен агентаwiki,eva: {base_url, token},verify_ssl,ca_bundle,timeout,poll_seconds,auto_update. Токен Eva — только здесь. Первый запуск делает владелец руками (свой процесс и блокировка). - Публикация версии для самообновления:
uv run sdlc-hub agent publish --module wiki ../eva-agent/src/agent.py(файл ложится вrelease/wiki/;min_agent_versionмодуля — настройкаwiki_min_agent_version). - Корень вики проекта:
project_createсwiki_rootили MCProot_set DOC-…/ форма на странице «Вики» (PATCH /projects/{метка}) — хаб ставитwiki_sync; дальше синхронизация раз вwiki_auto_sync_hours(по умолчанию 24,0— выключена), кнопка «Обновить» или MCPsync.
Копия лежит в ~/.local/share/sdlc-hub/wiki/<метка>/ (Markdown, по каталогу на страницу). Настройки модуля: wiki_pacing (пауза агента между
запросами к Eva, с), wiki_auto_sync_hours, wiki_min_agent_version.
Транспорт через общую папку
Если сеть между виртуалкой и Mac mini закрыта (VPN «Континент ZTN» на виртуалке режет локальную сеть), агент ходит
в хаб через общую папку UTM. Реле работает внутри serve и включается настройкой:
TOKEN=$(uv run sdlc-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 sdlc-hub agent publish ../jira-agent/src/agent.py # модуль jira (по умолчанию)
uv run sdlc-hub agent publish --module wiki ../eva-agent/src/agent.py
Версия берётся из строки AGENT_VERSION = N и должна быть больше опубликованной; файл и manifest.json ложатся в
~/.local/share/sdlc-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 |
агент | загрузка состояния задач пачками; агент v15 — у задач rank, story_points, sprint_ids (заменяет спринты задачи), в пачке boards (доска заменяет свою строку и, если sprints — список, свои спринты; тип доски пишется проектам с этой доской); без полей — данные прежние; неверная форма — 422 bad_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, PATCH /projects/{метка} |
клиент | проекты {projects: […]}: метка, название, created_at и поля модулей — jira: board_id, board_type, issues_count, jira_synced_at (время последнего полного обновления из Jira; null — ещё не было), wiki: wiki_root, wiki_synced_at, wiki_sync_error. POST заводит {label, name, поля модулей?, author?} → 201 {project, batch_id, batches}: пакеты, поставленные модулями (batches: [{module, id}]; у jira — «Новый проект <метка>», refresh по общей JQL; batch_id — пакет первого модуля); 409 project_exists, 422 bad_project, 422 bad_batch (постановка отказана). PATCH меняет изменяемые поля модулей (иные — 422 bad_project) → 200 {project, batch_ids, batches} |
GET /issues |
клиент | фильтры project (метка; неизвестная — 404), sprint, assignee, work_kind, 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): снимок для доски — свои задачи проекта, соседи (связи в обе стороны) и эпики своих задач, с work_kind из заметок; связи — те, у которых хотя бы один конец — своя задача проекта (без project — все задачи и связи), last_sync — последний успешный полный прогон |
GET /agile |
клиент | ?project=<метка> (без — 422, неизвестная — 404): доска проекта (board, тип из проекта; null — доски нет), все спринты доски со своими задачами и суммами оценок (count, count_done, unestimated, points, points_done), backlog (scrum — свои задачи вне активных и будущих спринтов, не выполненные; kanban — первая колонка, если бэклог доски включён), board_issues (kanban: остальные колонки, выполненные — за 14 дней); списки — по рангу |
GET /, GET /links |
— | перенаправление на /app/ («Обзор») |
PUT /issues/{key}/note |
клиент | заметки (частично) |
POST /batches, GET /batches, GET /batches/{id} |
клиент | пакеты команд; project в теле POST /batches — пакет проекта (проверки метки, ограничение задач проектом), ?project= у GET /batches — только пакеты проекта, ?module=<имя> — только пакеты модуля (по типу первой команды; неизвестный модуль — 404), limit; в каждом пакете поле module (null — не определён) |
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 |
TOKEN=$(uv run sdlc-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/?project=<метка> (карточки модулей Jira и «Вики» с состоянием агента, свежестью данных и обновлением, последние пакеты проекта), «Связи» (доска связей) /app/jira/links?project=<метка>, задачи /app/jira/issues?project=<метка>, лента изменений /app/jira/feed?project=<метка> (период и виды событий, история — и в карточке задачи), вики /app/wiki?project=<метка>, карточка /app/jira/issues/{key} — окно поверх списка или доски (закрытие — ✕, Esc, щелчок по фону; прямой адрес открывает окно поверх списка задач); / и
/links перенаправляют на «Обзор»; прежние адреса /app/board, /app/jira/board, /app/issues, /app/feed ведут на страницы модуля Jira; пункты меню могут зависеть от проекта (NavItem.when). Вход по клиентскому токену (uv run sdlc-hub tokens), токен хранится в браузере;
localhost и 127.0.0.1 — разные сайты для браузера, токен и раскладка у каждого свои. Выкатка на Mac mini:
cd ~/projects/jira-bridge/sdlc-hub
git pull
launchctl kickstart -k gui/$(id -u)/ru.aepif.sdlc-hub
make web
Порядок важен для версий с новой схемой (v7 — модуль wiki, v8 — без статуса по факту): сразу после git pull — kickstart,
потом make web. До kickstart не запускать MCP и CLI sdlc-hub (tokens, mcp, import): они открывают базу и
переводят её в новую схему, а хаб на прежнем коде с такой базой не работает. Старый фронт с новым API совместим, сборка может подождать. Откат на копию v6 из backups/ —
вместе с возвратом release/jira/* в release/ (прежняя раскладка), иначе старый код не найдёт публикацию агента.
Экраны доски (только просмотр: ни перетаскивания, ни кнопок спринтов, ни смены статуса или ранга): «Бэклог»
/app/jira/backlog?project=<метка> (активные и будущие спринты, бэклог, «Прошлые спринты (N)»), «Спринт»
/app/jira/sprint?project=<метка>[&sprint=<id>] (колонки доски активного спринта; устаревший sprint — первый активный),
«Kanban» /app/jira/kanban?project=<метка> (колонки доски, «Бэклог (N)» при включённом бэклоге Jira, выполненное за 14 дней).
Пункты меню зависят от типа доски проекта: у scrum-проекта «Бэклог» и «Спринт», у kanban-проекта «Kanban»; пока тип не известен,
пунктов нет. Адрес страницы чужого типа показывает «У проекта нет scrum-доски» / «У проекта нет kanban-доски» со ссылкой
«Связи». Три состояния данных: «Нужен агент v15 и обновление из Jira» (доска ещё не прочитана), «Не удалось прочитать настройки
доски из Jira» (колонок нет), сами данные. Карточка задачи открывается окном поверх экрана.
Проект выбирается выпадающим списком «Проект» в верхней панели (метка и название); пункт «+ Новый проект…» открывает окно
(метка, название, необязательно номер доски 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, story_points (число ≥ 0 или null — очистить) |
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 — доски проектов |
ensure_sprint_meta |
sprint (имя), необязательные rename_to, start_date, end_date (ГГГГ-ММ-ДД), goal; board_id хаб дописывает при постановке; результат {sprint_id, created} |
ensure_sprint_state |
sprint, state (active или closed), move_open_to (имя спринта или backlog, обязателен при closed, при active — 422); результат {sprint_id, moved: [ключ…]} |
ensure_rank |
key и ровно одно из before, after (ключ другой задачи); результат {changed} |
Тип 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: пакет с ensure_sprint_meta/ensure_sprint_state получает хвост по JQL проекта пакета; иначе — по затронутым ключам. Пример пакета (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, ensure_sprint_meta, ensure_sprint_state, ensure_rank
30 с, refresh 2 с (настройка pacing).
Команды спринта (ensure_sprint_meta, ensure_sprint_state) ставятся только в пакете проекта (поле project); ensure_rank — в любом пакете.
У команд спринта board_id дописывает хаб по проекту пакета; пакет без проекта — 422 «команды спринта — только в пакете проекта»;
доски нет, kanban или тип не известен — 422, как у ensure_sprint. В пакете проекта у ensure_rank key должен быть своей задачей.
Даты проверяются хабом: настоящая дата, начало не позже конца.
ensure_attachment ставится, только если файл уже загружен (POST /files), иначе 422 «команда N: файл не
загружен: POST /files». Агент ищет у задачи вложение с тем же именем и размером; нет — скачивает файл у хаба
(GET /agent/files/{sha256}) и прикладывает. По зеркалу команда не подтверждается (вложений в зеркале нет).
Пример сценария
sdlc-hub serveна Mac mini, агент запущен на виртуалке.- В Claude:
refreshсfullзагружает задачи в зеркало;sdlc-hub importпереносит заметки из CSV. batch_createставит пакет;batch_waitждёт результата;issueпоказывает итог.
Эксплуатация
- Выкатка спринтов и бэклога (порядок обязателен):
git pull→launchctl kickstart -k gui/$(id -u)/ru.aepif.sdlc-hub(владелец; миграция схемы v9) →make web→/mcpreconnect →agent publishагента v15 (uv run sdlc-hub agent publish ../jira-agent/src/agent.py) → проверка полей Jira банка (ранг, оценка, спринты) →field_ids.story_pointsв настройках, если агент поле оценки не нашёл.min_agent_versionподнимает владелец после выкатки v15 и после того, какagent_statusпокажет новую версию. Ограничение (R-33): до публикации агента v15story_pointsвensure_fieldsагент v14 выполнит какdoneс пустымchanged; v15 публикуется сразу после перезапуска хаба. - Резервные копии: перед первой записывающей командой пакета хаб копирует базу в
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.sdlc-hub.plist, лог~/Library/Logs/sdlc-hub.log
<?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.sdlc-hub</string>
<key>ProgramArguments</key><array>
<string>/opt/homebrew/bin/uv</string><string>run</string><string>--project</string>
<string>/Users/andrey.epifancev/projects/jira-bridge/sdlc-hub</string>
<string>sdlc-hub</string><string>serve</string>
</array>
<key>EnvironmentVariables</key><dict>
<key>PATH</key><string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
<key>PYTHONUNBUFFERED</key><string>1</string>
</dict>
<key>RunAtLoad</key><true/>
<key>KeepAlive</key><true/>
<key>StandardOutPath</key><string>/Users/andrey.epifancev/Library/Logs/sdlc-hub.log</string>
<key>StandardErrorPath</key><string>/Users/andrey.epifancev/Library/Logs/sdlc-hub.log</string>
</dict></plist>
Загрузить: launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/ru.aepif.sdlc-hub.plist. Перезапуск: launchctl kickstart -k gui/$(id -u)/ru.aepif.sdlc-hub.
Переключение с jira-hub
Одноразовое переключение живой установки (хаб назывался jira-hub) делает скрипт scripts/cutover.sh (обёртка над
scripts/cutover.py, stdlib). Выполняет владелец; без --apply скрипт только проверяет и печатает шаги.
- Слить PR переименования; переименовать репозиторий в Gitea
aep/jira-hub->aep/sdlc-hub. Gitea не перенаправляет git по SSH со старого имени: сразу сменить адресorigin(общий для checkout и всех worktree) —git -C ~/projects/jira-bridge/jira-hub remote set-url origin ssh://git@git.aepif.ru:2222/aep/sdlc-hub.git. - Закрыть или не трогать сессии Claude с MCP хаба (после переключения --
/mcpreconnect). git -C ~/projects/jira-bridge/jira-hub pull --ff-only, сразуbash ~/projects/jira-bridge/jira-hub/scripts/cutover.sh-- сухой прогон, проверить вывод (шаг 1 скрипта: чистый checkout наmain=mainвaep/sdlc-hub, доступенaep/sdlc-hub, нет целевых каталогов, естьuv,make,claude,launchctl,node,npm; неотслеживаемая старая сборкаsrc/jira_hub/не мешает, на шаге 4 она удаляется; адрес новогоoriginберётся из прежнего с заменой имени репозитория, протокол (ssh/https) сохраняется). Скрипт также предупреждает об открытых сессиях Claude со старым MCP (pgrep -fl 'jira-hub mcp'): после шага 3 они заново создают~/.local/share/jira-hub. Запускать скрипт лучше из обычного терминала вне checkout. Между pull и переключением работающий хаб живёт на загруженном коде, а новые сессии Claude не поднимут MCP -- окно в несколько минут.bash ~/projects/jira-bridge/jira-hub/scripts/cutover.sh --apply. Шаги скрипта, останавливается на первой ошибке: 2. остановитьru.aepif.jira-hub, дождаться закрытия порта 8790, переименовать его plist вru.aepif.jira-hub.plist.pre-sdlc; 3.~/.local/share/jira-hub->~/.local/share/sdlc-hub; 4. checkoutjira-bridge/jira-hub->jira-bridge/sdlc-hub, удалить неотслеживаемыйsrc/jira_hub,git worktree repair(включая.wt/*),origin->aep/sdlc-hub, новый.venv,uv sync,make web; 5. поставитьru.aepif.sdlc-hub(plist выше), запустить, дождаться порта; 6. править.mcp.json,.claude/settings*.json,CLAUDE.md,.claude/skillsв~/projects/awp-ca,~/projects/stocks_drrkи в каждом~/projects/*сjira-hubв.mcp.json(напримерsfm_foxpro; копии рядом:*.pre-sdlc); 7. user scope:claude mcp add -s user sdlc-jira -- uv run --project <checkout> sdlc-hub mcp jira, затемclaude mcp remove -s user jira-hub.- Просмотреть правки в awp-ca и stocks_drrk, проверить оставшиеся упоминания
jira-hubиз вывода скрипта, удалить копии*.pre-sdlc(это не git-репозитории -- коммитить нечего)./mcpreconnect;codegraph initиuv syncв новом checkout и в каждом worktree.wt/*; после запуска -- лог реле иagent_status(при «Operation not permitted» выдать python из нового.venvполный доступ к диску заново). - Проверить:
agent_statusчерезsdlc-jira,statusчерезsdlc-wiki, вебhttp://127.0.0.1:8790/app/без повторного входа (токен и раскладка доски переносятся из прежних ключей браузерного хранилища).
В уже существующем окружении (.venv старого checkout) после переезда пакета нужен uv sync --reinstall-package sdlc-hub
(скрипт пересоздаёт .venv сам).
Откат
Если скрипт остановился или хаб не поднялся (скрипт печатает «Откат: …» со своими путями):
- Остановить новый хаб:
launchctl bootout gui/$(id -u)/ru.aepif.sdlc-hub; удалить~/Library/LaunchAgents/ru.aepif.sdlc-hub.plist. - Если
~/.local/share/jira-hubпоявился заново (сессия Claude со старым MCP) -- удалить его передmv. Вернуть каталоги:mv ~/.local/share/sdlc-hub ~/.local/share/jira-hub;mv ~/projects/jira-bridge/sdlc-hub ~/projects/jira-bridge/jira-hub, затемgit -C ~/projects/jira-bridge/jira-hub worktree repair(и пути всех.wt/*). - Вернуть
originнаaep/jira-hub(git remote set-url origin …; если репозиторий в Gitea уже переименован -- адрес прежнего имени перенаправляется, но лучше убедиться) иgit checkoutкоммитаmainперед слиянием PRsdlc-rename(найти вgit log);uv syncи при необходимостиmake web. - Вернуть прежний plist (скрипт оставил копию
ru.aepif.jira-hub.plist.pre-sdlc:mvбез суффикса; иначе создать заново по тексту ниже) и загрузить его (launchctl bootstrap gui/$(id -u) …):
<?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>/opt/homebrew/bin/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>
<key>PYTHONUNBUFFERED</key><string>1</string>
</dict>
<key>RunAtLoad</key><true/>
<key>KeepAlive</key><true/>
<key>StandardOutPath</key><string>/Users/andrey.epifancev/Library/Logs/jira-hub.log</string>
<key>StandardErrorPath</key><string>/Users/andrey.epifancev/Library/Logs/jira-hub.log</string>
</dict></plist>
- Вернуть файлы проектов из копий: в каждом проекте-потребителе (awp-ca, stocks_drrk, sfm_foxpro и т. д.) каждый
*.pre-sdlcпереместить поверх оригинала (mv файл.pre-sdlc файл). - Вернуть MCP:
claude mcp add -s user jira-hub -- uv run --project ~/projects/jira-bridge/jira-hub jira-hub mcp, затемclaude mcp remove -s user sdlc-jira;/mcpreconnect.
Тесты
uv run pytest