446 lines
47 KiB
Markdown
446 lines
47 KiB
Markdown
# 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`, `attachment`, `sync`, `status`. Пока у проекта нет корня
|
||
вики (`root_set DOC-…` или `wiki_root` в `project_create`), они отвечают «у проекта <метка> нет корня вики».
|
||
|
||
Сессия 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:<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 <корень хаба> 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/<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 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<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`, `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`, `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/` («Обзор») |
|
||
| `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 |
|
||
|
||
```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/board?project=<метка>`, задачи `/app/jira/issues?project=<метка>`, лента изменений `/app/jira/feed?project=<метка>` (период и виды событий, история — и в карточке задачи), вики `/app/wiki?project=<метка>`, карточка `/app/jira/issues/{key}` — окно поверх списка или доски (закрытие — ✕, Esc, щелчок по фону; прямой адрес открывает окно поверх списка задач); `/` и
|
||
`/links` перенаправляют на «Обзор»; прежние адреса `/app/board`, `/app/issues`, `/app/feed` ведут на страницы модуля Jira. Вход по клиентскому токену (`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): сразу после `git pull` — kickstart, потом `make web`. До kickstart не
|
||
запускать MCP и CLI `sdlc-hub` (`tokens`, `mcp`, `import`): они открывают базу и переводят её в v7, а хаб на прежнем коде
|
||
с такой базой не работает. Старый фронт с новым API совместим, сборка может подождать. Откат на копию v6 из `backups/` —
|
||
вместе с возвратом `release/jira/*` в `release/` (прежняя раскладка), иначе старый код не найдёт публикацию агента.
|
||
|
||
Проект выбирается выпадающим списком «Проект» в верхней панели (метка и название); пункт «+ Новый проект…» открывает окно
|
||
(метка, название, необязательно номер доски 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. `sdlc-hub serve` на Mac mini, агент запущен на виртуалке.
|
||
2. В Claude: `refresh` с `full` загружает задачи в зеркало; `sdlc-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.sdlc-hub.plist`, лог `~/Library/Logs/sdlc-hub.log`
|
||
|
||
```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.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` скрипт только проверяет и печатает шаги.
|
||
|
||
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 <checkout> 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
|
||
<?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>
|
||
```
|
||
|
||
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
|
||
```
|