# 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). ## Установка ```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/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. ## Первый запуск ```bash 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 ```bash 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: ```bash 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`, с привязкой к проекту): ```json { "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`, `changes`, `diff`, `attachment`, `sync`, `status`. Пока у проекта нет корня вики (`root_set DOC-…` или `wiki_root` в `project_create`), они отвечают «у проекта <метка> нет корня вики». - `changes [hours] [since] [until] [author]` — правки страниц за период (по умолчанию — с начала суток), по страницам, новые сверху; - `diff code [version] [offset]` — дифф правки (без `version` — последняя), по частям 20 000 символов с `продолжение: offset=`; - `page` — шапка с «изменено: ДД.ММ ЧЧ:ММ <автор> (версий: N)»; `sync` — коротко: «поставлено: пакет N», «уже идёт: пакет N», с `wait` — «ок: синхронизировано …» или «ошибка: …»; - `status` — дополнительно строка «история: <время>» или «история не загружена». `page`, `changes`, `diff` команд не ставят: читают копию хаба. Спринты и бэклог (нужна привязка к проекту; читают `GET /agile` проекта сессии): `sprints` — спринты доски с суммами задач и SP «выполнено/всего»; `sprint [name]` — спринт (по умолчанию активный) с задачами по колонкам доски; `backlog [limit]` — бэклог по рангу (по умолчанию 50 строк). В `issues` есть колонка SP. Команды спринтов и ранга ставятся через `batch_create`, например смена состояния спринта с переносом открытых задач (хвост пакета хаб заменит на `refresh` по JQL проекта): ```json {"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` репозитория проекта (метка сравнивается точно, с учётом регистра): ```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:`), `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 МБ — ответ с причиной и путём, пакет не ставится. ```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"}}] ``` ## Вики и eva-agent Копию вики Eva хаб получает от `eva-agent` — отдельного агента на той же виртуалке (отдельный репозиторий `aep/eva-agent`, каталог `jira-bridge/eva-agent/`; Python 3.9, один файл `src/agent.py` + `run.py` — как jira-agent). Хаб в Eva не ходит и токен Eva не хранит. 1. `uv run sdlc-hub tokens` — взять токен агента `wiki` (у `jira` и `wiki` токены разные; чужой токен на маршрутах другого агента даёт 401). 2. На виртуалке отдельным каталогом положить 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 — только здесь. Первый запуск делает владелец руками (свой процесс и блокировка). 3. Публикация версии для самообновления: `uv run sdlc-hub agent publish --module wiki ../eva-agent/src/agent.py` (файл ложится в `release/wiki/`; `min_agent_version` модуля — настройка `wiki_min_agent_version`). 4. Корень вики проекта: `project_create` с `wiki_root` или MCP `root_set DOC-…` / форма на странице «Вики» (`PATCH /projects/{метка}`) — хаб ставит `wiki_sync`; дальше синхронизация раз в `wiki_auto_sync_hours` (по умолчанию 24, `0` — выключена), кнопка «Обновить» или MCP `sync`. Копия лежит в `~/.local/share/sdlc-hub/wiki/<метка>/` (Markdown, по каталогу на страницу). Настройки модуля: `wiki_pacing` (пауза агента между запросами к Eva, с), `wiki_auto_sync_hours`, `wiki_min_agent_version`. ## Транспорт через общую папку Если сеть между виртуалкой и Mac mini закрыта (VPN «Континент ZTN» на виртуалке режет локальную сеть), агент ходит в хаб через общую папку UTM. Реле работает внутри `serve` и включается настройкой: ```bash 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/.json` (`method`, `path`, `body`, токен, `deadline`), реле переименованием забирает его в `work/`, пересылает в свой HTTP и кладёт ответ `resp/.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 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 /wiki/changes` | клиент | лента правок вики проекта: `project`, `since`, `until` (ISO со смещением или `Z`, иначе 422; по умолчанию — последние 24 ч), `author` (подстрока имени или логина, без учёта регистра), `limit` (1–2000, по умолчанию 500) → `{since, until, synced_at, history_at, truncated, changes}` | | `GET /wiki/pages/{code}/versions` | клиент | версии страницы, новые первыми: `{versions: [{version, at, author, added, removed}]}`; страницы нет в копии — 404 | | `GET /wiki/pages/{code}/diff` | клиент | дифф версии (`version`, по умолчанию последняя) относительно предыдущей, по частям `offset` (части по 20 000 символов, `next_offset`); нет версий — 404 `no_versions`, нет такой версии — 404 `no_version` | | `GET /wiki/status` | клиент | состояние синхронизации вики проекта, в том числе `history_at` (когда загружена история; `null` — ещё не загружалась) | | `GET /wiki/pages/{code}` | клиент | страница копии, в том числе `modified_at`, `modified_by` (`{login, name}` или `null`) и `versions` — последняя правка и число версий | | `GET /settings`, `PUT /settings` | клиент | настройки (PUT частично, поднимает `settings_version`; ответ — все настройки после записи, как у GET); `base_jql` вычисляется из проектов (без проектов — `null`), запись `base_jql` и `board_id` — 422 | ```bash 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/feed?project=<метка>` (источники Jira и Вики, период и виды событий Jira, история — и в карточке задачи), вики `/app/wiki?project=<метка>`, карточка `/app/jira/issues/{key}` — окно поверх списка или доски (закрытие — ✕, Esc, щелчок по фону; прямой адрес открывает окно поверх списка задач); `/` и `/links` перенаправляют на «Обзор»; прежние адреса `/app/board`, `/app/jira/board`, `/app/issues` ведут на страницы модуля Jira, `/app/jira/feed` — на `/app/feed`; пункты меню могут зависеть от проекта (`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=]` (колонки доски активного спринта; устаревший `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:`), `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:`), `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:`: команда выдаётся после `ensure_issue` с тем же `temp_key` из того же пакета. В хвост пакета хаб сам добавляет `refresh`: пакет с `ensure_sprint_meta`/`ensure_sprint_state` получает хвост по JQL проекта пакета; иначе — по затронутым ключам. Пример пакета (`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`, `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}`) и прикладывает. По зеркалу команда не подтверждается (вложений в зеркале нет). ## Пример сценария 1. `sdlc-hub serve` на Mac mini, агент запущен на виртуалке. 2. В Claude: `refresh` с `full` загружает задачи в зеркало; `sdlc-hub import` переносит заметки из CSV. 3. `batch_create` ставит пакет; `batch_wait` ждёт результата; `issue` показывает итог. ## Эксплуатация - Выкатка спринтов и бэклога (порядок обязателен): `git pull` → `launchctl kickstart -k gui/$(id -u)/ru.aepif.sdlc-hub` (владелец; миграция схемы v9) → `make web` → `/mcp` reconnect → `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): до публикации агента v15 `story_points` в `ensure_fields` агент v14 выполнит как `done` с пустым `changed`; v15 публикуется сразу после перезапуска хаба. - Резервные копии: перед первой записывающей командой пакета хаб копирует базу в `backups/<дата-время>-.sqlite` и хранит последние `backup_keep` копий (по умолчанию 30). - Файлы вложений: `files/`, одна копия на содержимое, в резервные копии базы не входят. Раз в сутки (первый раз — через минуту после старта) хаб удаляет файлы старше 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 Labelru.aepif.sdlc-hub ProgramArguments /opt/homebrew/bin/uvrun--project /Users/andrey.epifancev/projects/jira-bridge/sdlc-hub sdlc-hubserve EnvironmentVariables PATH/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin PYTHONUNBUFFERED1 RunAtLoad KeepAlive StandardOutPath/Users/andrey.epifancev/Library/Logs/sdlc-hub.log StandardErrorPath/Users/andrey.epifancev/Library/Logs/sdlc-hub.log ``` Загрузить: `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` скрипт только проверяет и печатает шаги. 1. Слить 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`. 2. Закрыть или не трогать сессии Claude с MCP хаба (после переключения -- `/mcp` reconnect). 3. `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 -- окно в несколько минут. 4. `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. checkout `jira-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 sdlc-hub mcp jira`, затем `claude mcp remove -s user jira-hub`. 5. Просмотреть правки в awp-ca и stocks_drrk, проверить оставшиеся упоминания `jira-hub` из вывода скрипта, удалить копии `*.pre-sdlc` (это не git-репозитории -- коммитить нечего). `/mcp` reconnect; `codegraph init` и `uv sync` в новом checkout и в каждом worktree `.wt/*`; после запуска -- лог реле и `agent_status` (при «Operation not permitted» выдать python из нового `.venv` полный доступ к диску заново). 6. Проверить: `agent_status` через `sdlc-jira`, `status` через `sdlc-wiki`, веб `http://127.0.0.1:8790/app/` без повторного входа (токен и раскладка доски переносятся из прежних ключей браузерного хранилища). В уже существующем окружении (`.venv` старого checkout) после переезда пакета нужен `uv sync --reinstall-package sdlc-hub` (скрипт пересоздаёт `.venv` сам). ### Откат Если скрипт остановился или хаб не поднялся (скрипт печатает «Откат: …» со своими путями): 1. Остановить новый хаб: `launchctl bootout gui/$(id -u)/ru.aepif.sdlc-hub`; удалить `~/Library/LaunchAgents/ru.aepif.sdlc-hub.plist`. 2. Если `~/.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/*`). 3. Вернуть `origin` на `aep/jira-hub` (`git remote set-url origin …`; если репозиторий в Gitea уже переименован -- адрес прежнего имени перенаправляется, но лучше убедиться) и `git checkout` коммита `main` перед слиянием PR `sdlc-rename` (найти в `git log`); `uv sync` и при необходимости `make web`. 4. Вернуть прежний plist (скрипт оставил копию `ru.aepif.jira-hub.plist.pre-sdlc`: `mv` без суффикса; иначе создать заново по тексту ниже) и загрузить его (`launchctl bootstrap gui/$(id -u) …`): ```xml Labelru.aepif.jira-hub ProgramArguments /opt/homebrew/bin/uvrun--project /Users/andrey.epifancev/projects/jira-bridge/jira-hub jira-hubserve EnvironmentVariables PATH/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin PYTHONUNBUFFERED1 RunAtLoad KeepAlive StandardOutPath/Users/andrey.epifancev/Library/Logs/jira-hub.log StandardErrorPath/Users/andrey.epifancev/Library/Logs/jira-hub.log ``` 5. Вернуть файлы проектов из копий: в каждом проекте-потребителе (awp-ca, stocks_drrk, sfm_foxpro и т. д.) каждый `*.pre-sdlc` переместить поверх оригинала (`mv файл.pre-sdlc файл`). 6. Вернуть 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`; `/mcp` reconnect. ## Тесты ```bash uv run pytest ```