From d7f82cb611189e033776e28c2fc7e58e329218ce Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sun, 11 Oct 2026 22:47:24 +0400 Subject: [PATCH] =?UTF-8?q?feat(hub):=20CLI,=20README,=20=D0=BE=D0=BF?= =?UTF-8?q?=D0=B8=D1=81=D0=B0=D0=BD=D0=B8=D0=B5=20=D0=B0=D1=80=D1=85=D0=B8?= =?UTF-8?q?=D1=82=D0=B5=D0=BA=D1=82=D1=83=D1=80=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_014Typ5sXWNMFirr2va2uL8o --- ARCHITECTURE.md | 32 +++++++- CLAUDE.md | 29 +++++--- README.md | 174 ++++++++++++++++++++++++++++++++++++++++++++ src/jira_hub/cli.py | 68 +++++++++++++++++ tests/test_cli.py | 25 +++++++ 5 files changed, 313 insertions(+), 15 deletions(-) create mode 100644 README.md create mode 100644 src/jira_hub/cli.py create mode 100644 tests/test_cli.py diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 4a85988..d77b461 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -1,15 +1,39 @@ # Архитектура -{{Карта модулей на одну страницу: что где лежит, границы, потоки данных. Читается агентом перед architectural brainstorming. Обновляется вместе со спеками.}} +Хаб моста Jira: SQLite-зеркало задач AURORA/AWPCA, служебные заметки, очередь команд, HTTP API и MCP. Агент (`jira-agent`, виртуалка) опрашивает хаб, пишет в Jira и возвращает состояние; хаб Jira не вызывает. ## Модули | Модуль | Ответственность | Зависит от | |---|---|---| -| | | | +| `paths` | каталог данных (`JIRA_HUB_HOME`), пути базы и `backups/` | stdlib | +| `db` | схема SQLite, миграции, `connect` | stdlib | +| `settings` | настройки (`setting`), токены, темп, перенос из `config.example.json` | `db` | +| `mirror` | зеркало задач и связей, `apply_state`, заметки, фильтры | `db` | +| `commands` | контракт типов команд, валидация, временные ключи, оценка, подтверждение по состоянию | `mirror` | +| `queue` | пакеты, выдача (`claim`), отчёты, отмена, повтор, автообновление | `commands`, `mirror`, `settings`, `db` | +| `backup` | копии файла базы, ротация | stdlib | +| `api` | HTTP-сервер, маршруты, авторизация по токенам, фоновый `tick` | `queue`, `mirror`, `settings`, `backup` | +| `client` | HTTP-клиент хаба | `db`, `paths`, `settings` (только для `from_home`) | +| `mcp_server` | инструменты MCP по stdio | `client` | +| `importer` | разовый импорт CSV-пакета в заметки, сверка с зеркалом | `mirror`, `settings` | +| `cli` | `init`, `serve`, `import`, `mcp`, `tokens` | `api`, `mcp_server`, `importer`, `settings`, `db`, `paths` | + +Цепочки: `api → queue → commands/mirror/settings → db`; `mcp_server → client → HTTP API`; `importer → mirror/settings`. + +## Потоки данных +- Запись в Jira: клиент (`mcp_server`/интерфейс) → `POST /batches` → `queue` → агент `claim` → Jira → `POST /agent/commands/{id}/result`. +- Чтение Jira: команда `refresh` → агент → `POST /agent/state` → `mirror.apply_state`. +- Токены: агентский — только `/agent/*`, клиентский — остальное; MCP берёт клиентский токен из базы через `HubClient.from_home`. ## Границы -- Что не должно импортировать что. +- Зеркало пишет только `mirror.apply_state` по запросу агента (и заметки через `PUT /issues/{key}/note`); из очереди и API задач напрямую не правят. +- Хаб не знает пароль Jira и не обращается к Jira; `init` не переносит `auth`, `base_url`, `verify_ssl`, `ca_bundle`. +- `queue` не повторяет команды: нет автоматических ретраев, повтор — новый пакет. +- `mcp_server` не импортирует `queue`/`mirror`/`db` напрямую и ходит только через `client`. +- `commands`, `mirror`, `queue` не знают про HTTP; HTTP живёт только в `api` и `client`. ## Открытые вопросы -- +- Просмотрщик на живых данных и правки из UI — отдельные change'и (`viewer-live`, `viewer-edit`). +- Автоподстройка темпа по фактам 429 не реализована: хаб только показывает их. +- Незакрытые SQLite-соединения и сокеты в ряде тестов (ResourceWarning при `-W error::ResourceWarning`). diff --git a/CLAUDE.md b/CLAUDE.md index 768e0b3..376ab86 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,28 +1,35 @@ # jira-hub ## Что это -{{ОДНИМ АБЗАЦЕМ: что делает система, для кого, главный сценарий.}} +Хаб моста Jira для АРМ КА: единственный источник истины по задачам проекта AURORA (метка AWPCA) на Mac mini (`192.168.0.106`). Хранит зеркало задач, служебные заметки и очередь команд в SQLite, отдаёт HTTP API в локальной сети (агенту на виртуалке `jira-agent` и интерфейсам) и MCP-сервер для Claude. Главный сценарий: из чата поставить пакет команд, агент на виртуалке выполняет их в Jira по правилу «проверь, потом сделай» и возвращает состояние задач в зеркало. Спека: `openspec/changes/bridge-core/`. ## Стек и ограничения -- {{Язык, сборка, тесты — например: Python 3.12, uv, pytest.}} -- {{Жёсткие ограничения среды исполнения — без чего код не должен собираться.}} +- Python 3.13, uv, hatchling, pytest; HTTP на stdlib (`http.server`), БД — SQLite (stdlib), MCP — пакет `mcp` (закреплён `<2`, рулинг R-10). +- Хаб не знает пароль Jira и не ходит в Jira: учётные данные и сеть до Jira только у агента. +- Зеркало задач пишет только `mirror.apply_state` по загрузке состояния от агента (`POST /agent/state`); правки идут командами через очередь. +- Хаб не повторяет команды сам: повтор — новая команда по запросу человека/интерфейса. +- Данные: `~/.local/share/jira-hub/` (`JIRA_HUB_HOME`): `hub.sqlite`, `backups/`. Порт по умолчанию 8790. +- Два статических токена (в таблице `setting`): агентский открывает только `/agent/*`, клиентский — остальное. ## Архитектура -- `src/core/` (или {{ваш путь}}) — чистая логика без зависимостей от фреймворка/платформы. Покрывается тестами полностью. -- `src/{{adapter}}/` — тонкая обёртка над платформой. Тестируется руками или интеграционно. -- {{Ключевые интерфейсы/порты между ними, если есть.}} +- `src/jira_hub/` — весь код пакета, без разделения на ядро и адаптеры; чистая логика (`commands`, `mirror`, `queue`, `settings`) принимает соединение SQLite параметром. +- `db` (схема, миграции) ← `settings`, `mirror`, `commands` ← `queue` ← `api` (HTTP, маршруты в `ROUTES`, фоновый `tick`) ← `cli`. +- `client` (HTTP-клиент хаба) ← `mcp_server` (stdio, инструменты MCP); `importer` → `mirror`/`settings`; `backup` вызывается из `api`. +- Подробная карта модулей и границ — `ARCHITECTURE.md`. ## Команды -- `{{npm test / pytest}}` — весь набор тестов. Зелёным считается только весь набор. -- `{{npm run build / ...}}` — сборка. -- `{{npm run dev / ...}}` — локальный запуск. +- `uv run pytest` — весь набор тестов. Зелёным считается только весь набор. +- `uv sync` — установка окружения. +- `uv run jira-hub init --config PATH [--force]` — настройки проекта из `config.example.json`, создание токенов. +- `uv run jira-hub serve [--host] [--port]` — локальный запуск хаба. +- `uv run jira-hub import FOLDER` — импорт CSV-пакета `admin/jira-sync`; `uv run jira-hub mcp` — MCP по stdio; `uv run jira-hub tokens` — показать токены. ## Тесты - Golden-тесты: `tests/fixtures/.*` + эталон `.expected.*`. Внешние сервисы в тестах мокаются. -- {{Где лежит тестовое окружение (vault, БД, песочница).}} +- SQLite в памяти или во временном `JIRA_HUB_HOME` (`tmp_path`); API поднимается на порту 0 в отдельном потоке. Jira и агент в тестах не участвуют. ## Первая подсистема -{{Что делается первым и почему. Тестовая инфраструктура — Task 1 первого плана.}} +Очередь команд (`queue.py`, Task 5): пакеты, порядок `(batch.created_at, seq)`, одна команда в полёте, временные ключи `new:`, подтверждение по состоянию зеркала. Остальное (API, MCP, импорт) надстраивается над ней. ## OpenSpec и superpowers - Вход в любую задачу — superpowers:brainstorming. Skills openspec-propose и openspec-explore сами по себе не запускать. diff --git a/README.md b/README.md new file mode 100644 index 0000000..6a124c2 --- /dev/null +++ b/README.md @@ -0,0 +1,174 @@ +# jira-hub + +Хаб моста Jira для АРМ КА: зеркало задач проекта AURORA (метка AWPCA) в SQLite, служебные заметки, очередь команд, +HTTP API в локальной сети и MCP-сервер для Claude. Хаб работает на Mac mini (`192.168.0.106`); в Jira ходит только +агент на виртуалке (репозиторий `jira-agent`), пароль Jira хабу неизвестен. +Спека: `openspec/changes/bridge-core/` (proposal, design, specs). + +## Установка + +```bash +uv sync +``` + +Данные лежат в `~/.local/share/jira-hub/` (каталог переопределяется переменной `JIRA_HUB_HOME`): +`hub.sqlite` и `backups/`. Пакет `mcp` закреплён версией `<2` (рулинг R-10): MCP SDK 2.x ломает API. + +## Первый запуск + +```bash +uv run jira-hub init --config ../admin/jira-sync/config.example.json +uv run jira-hub serve # 0.0.0.0:8790 +``` + +`init` переносит в настройки хаба всё, что относится к проекту Jira (поля, типы, карту переходов, типы связей, доску, +приоритеты), и создаёт два токена: агентский и клиентский. Адрес и учётные данные Jira (`base_url`, `auth`, `verify_ssl`, +`ca_bundle`) остаются у агента и в хаб не попадают. Повторный `init` отказывает ("уже инициализирован"); с `--force` +настройки переписываются. Токены можно показать позже: `uv run jira-hub tokens`. + +Передать агенту нужно адрес хаба и агентский токен: через общую папку виртуалки положить в `agent.json` +`"hub_url": "http://192.168.0.106:8790"` и `"hub_token": "<агентский токен>"`. Клиентский токен остаётся на этой машине: +его использует MCP (читает из базы сам). + +Порт меняется ключом `--port` или настройкой `port`; адрес прослушивания — `--host`. + +## Импорт пакета admin/jira-sync + +```bash +uv run jira-hub import ../admin/jira-sync +``` + +Переносит из CSV заметки (статус по факту, вид работы и т. п.) и выводит отчёт JSON: +`notes` (сколько заметок записано), `expected_links`, `missing_keys` (ключи из CSV, которых нет в зеркале) и +`missing_links` (ожидаемые связи, которых нет). Запускать после первого `refresh` с `full: true`: пока зеркало пусто, +все ключи будут в `missing_keys`. Заметки к ещё не загруженным задачам сохраняются. + +## MCP в Claude + +`jira-hub mcp` запускает MCP-сервер по stdio; он ходит в API хаба клиентским токеном. Если хаб не запущен, инструменты +отвечают «хаб не запущен: jira-hub serve». Регистрация вручную (файл `~/.claude.json` скрипты хаба не правят), +блок для проекта `awp-ca`: + +```json +"jira-hub": {"type": "stdio", "command": "uv", "args": ["run", "--project", "/Users/andrey.epifancev/projects/awp-ca/jira-hub", "jira-hub", "mcp"]} +``` + +Либо командой: `claude mcp add jira-hub -- uv run --project /Users/andrey.epifancev/projects/awp-ca/jira-hub jira-hub mcp`. + +| Инструмент | Параметры | Результат | +|---|---|---| +| `issues` | фильтры как у `GET /issues`, `limit` | таблица задач | +| `issue` | `key` | поля, связи, заметки, последние команды | +| `notes_set` | `key`, поля заметок | обновлённые заметки | +| `batch_create` | `title`, `commands` | `id`, оценка длительности, число команд | +| `batch_status` | `id` | статусы команд, ошибки, оценка остатка | +| `batch_wait` | `id`, `timeout_minutes` (до 60) | как `batch_status`, по завершении или таймауту | +| `batches` | `limit` | последние пакеты | +| `command_retry`, `batch_cancel` | `id` | новый пакет / результат отмены | +| `refresh` | `keys` или `full`, `wait` | `sync_run` или id пакета | +| `agent_status` | нет | онлайн/офлайн, версия, текущая команда, 429 за сутки | +| `sync_log` | `limit` | последние загрузки состояния | +| `settings_get`, `settings_set` | ключ, значение | значение / новая `settings_version` | + +Автор команд из MCP: `claude`; из API без автора: `api`. + +## HTTP API + +Все маршруты требуют `Authorization: Bearer <токен>`; ответы JSON, ошибки `{error, detail}` (400/401/404/409/422). + +| Маршрут | Токен | Назначение | +|---|---|---| +| `POST /agent/claim` | агент | взять следующую команду (долгий запрос до 30 с) | +| `GET /agent/settings` | агент | настройки для агента (без токенов) | +| `GET /agent/commands/{id}` | агент | команда по id | +| `POST /agent/commands/{id}/result` | агент | отчёт о выполнении (идемпотентно) | +| `POST /agent/state` | агент | загрузка состояния задач пачками | +| `GET /agent/known-keys` | агент | ключи зеркала | +| `GET /issues` | клиент | фильтры `sprint`, `assignee`, `work_kind`, `fact_status`, `status`, `label`, `epic`, `neighbor`, `q`, `limit`, `offset` | +| `GET /issues/{key}` | клиент | задача целиком | +| `PUT /issues/{key}/note` | клиент | заметки (частично) | +| `POST /batches`, `GET /batches`, `GET /batches/{id}` | клиент | пакеты команд | +| `POST /batches/{id}/cancel` | клиент | отмена пакета | +| `POST /commands/{id}/retry`, `POST /commands/{id}/cancel` | клиент | повтор (новый пакет) / отмена (`force` для взятой) | +| `POST /refresh` | клиент | `{keys}`, `{jql}` или `{full: true}` | +| `GET /agent`, `GET /sync-runs` | клиент | состояние агента, журнал загрузок | +| `GET /settings`, `PUT /settings` | клиент | настройки (PUT частично, поднимает `settings_version`) | + +```bash +TOKEN=$(uv run jira-hub tokens | sed -n 's/^клиентский токен: //p') +curl -s -H "Authorization: Bearer $TOKEN" "http://127.0.0.1:8790/issues?sprint=Спринт%202&limit=20" +``` + +## Контракт команд + +Каждая команда описывает желаемое состояние одной задачи; агент читает, сравнивает и пишет только разницу, повтор с тем же +результатом завершается `done` без записи. + +| Тип | Параметры | +|---|---| +| `ensure_issue` | `temp_key` (`new:`), `issue_type` (`task`/`epic`), `summary`, `description`, `priority`, `duedate`, `planned_start`, `planned_end`, `labels`, `epic_key`, `assignee` | +| `ensure_fields` | `key` и подмножество: `summary`, `description`, `priority`, `duedate`, `planned_start`, `planned_end`, `labels_add`, `labels_remove`, `epic_key`, `assignee` | +| `ensure_comment` | `key`, `marker`, `body` | +| `ensure_status` | `key`, `target` (имя статуса или список-путь) | +| `ensure_link` | `source`, `target`, `link_type`, `replace` | +| `ensure_sprint` | `key`, `sprint` (имя или `backlog`) | +| `refresh` | `keys` или `jql` | + +Ключ в параметре может быть временным `new:`: команда выдаётся после `ensure_issue` с тем же `temp_key` из того же +пакета. В хвост пакета хаб сам добавляет `refresh` по затронутым ключам. Пример пакета (`POST /batches`): + +```json +{"title": "1538 в спринт 2", "author": "claude", "commands": [ + {"type": "ensure_fields", "params": {"key": "AURORA-1538", "duedate": "2026-10-23"}}, + {"type": "ensure_link", "params": {"source": "AURORA-1537", "target": "AURORA-1538", "link_type": "связанные", "replace": true}}, + {"type": "ensure_sprint", "params": {"key": "AURORA-1538", "sprint": "Спринт 2"}} +]} +``` + +Ответ: `{id, estimate_seconds}`. Зазоры между записями в Jira по умолчанию: `ensure_issue` и `ensure_fields` 120 с, +`ensure_comment`, `ensure_status`, `ensure_link`, `ensure_sprint` 30 с, `refresh` 2 с (настройка `pacing`). + +## Пример сценария + +1. `jira-hub serve` на Mac mini, агент запущен на виртуалке. +2. В Claude: `refresh` с `full` загружает задачи в зеркало; `jira-hub import` переносит заметки из CSV. +3. `batch_create` ставит пакет; `batch_wait` ждёт результата; `issue` показывает итог. + +## Эксплуатация + +- Резервные копии: перед первой записывающей командой пакета хаб копирует базу в `backups/<дата-время>-.sqlite` + и хранит последние `backup_keep` копий (по умолчанию 30). +- Автообновление: если `auto_refresh_minutes > 0` (по умолчанию 60), агент на связи и очередь пуста дольше интервала, + хаб ставит пакет «Автообновление». +- Настройки: `PUT /settings` или инструмент `settings_set`; защищены `settings_version`, `schema_version`, `tokens`. +- Команда «нет ответа от агента»: команда в `claimed` без отчёта. Дождитесь агента (перезапущенный агент отчитается) или + отмените: `POST /commands/{id}/cancel` с `{"force": true}`. Хаб сам команды не повторяет; если факт уже виден после + `refresh`, команда становится `confirmed_by_state`. Повтор: `command_retry`. +- Автозапуск `serve` через launchd: `~/Library/LaunchAgents/ru.aepif.jira-hub.plist` + +```xml + + + + Labelru.aepif.jira-hub + ProgramArguments + uvrun--project + /Users/andrey.epifancev/projects/awp-ca/jira-hub + jira-hubserve + + EnvironmentVariables + PATH/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin + + RunAtLoad + KeepAlive + StandardErrorPath/tmp/jira-hub.err.log + +``` + +Загрузить: `launchctl load ~/Library/LaunchAgents/ru.aepif.jira-hub.plist`. + +## Тесты + +```bash +uv run pytest +``` diff --git a/src/jira_hub/cli.py b/src/jira_hub/cli.py new file mode 100644 index 0000000..7a51962 --- /dev/null +++ b/src/jira_hub/cli.py @@ -0,0 +1,68 @@ +"""Команды jira-hub: init (настройки из config.example.json), serve (HTTP), import (CSV-пакет), mcp (stdio), tokens.""" +import argparse +import json +import sys + +from . import db, importer, paths, settings + + +def main(argv=None) -> int: + ap = argparse.ArgumentParser(prog="jira-hub", description="Мост Jira для АРМ КА: хаб") + sub = ap.add_subparsers(dest="cmd", required=True) + p = sub.add_parser("init", help="перенести настройки проекта из config.example.json, создать токены") + p.add_argument("--config", required=True) + p.add_argument("--force", action="store_true") + p = sub.add_parser("serve", help="запустить HTTP API") + p.add_argument("--host", default="0.0.0.0") + p.add_argument("--port", type=int) + p = sub.add_parser("import", help="импорт CSV-пакета admin/jira-sync в заметки") + p.add_argument("folder") + sub.add_parser("mcp", help="MCP-сервер по stdio") + sub.add_parser("tokens", help="показать токены агента и клиента") + args = ap.parse_args(argv) + + if args.cmd == "serve": + from . import api + hub = api.Hub(paths.db_path()) + conn = hub.conn() + port = args.port or int(settings.get(conn, "port") or 8790) + conn.close() + api.serve(hub, args.host, port) + return 0 + if args.cmd == "mcp": + from . import mcp_server + mcp_server.run() + return 0 + + conn = db.connect(paths.db_path()) + settings.ensure_defaults(conn) + try: + if args.cmd == "init": + with open(args.config, encoding="utf-8") as f: + cfg = json.load(f) + try: + keys = settings.init_from_config(conn, cfg, force=args.force) + except RuntimeError as e: + print(e, file=sys.stderr) + return 1 + print("перенесено: " + ", ".join(keys)) + _print_tokens(conn) + return 0 + if args.cmd == "import": + print(json.dumps(importer.import_package(conn, args.folder), ensure_ascii=False, indent=1)) + return 0 + if args.cmd == "tokens": + _print_tokens(conn) + return 0 + finally: + conn.close() + return 2 + + +def _print_tokens(conn) -> None: + tok = settings.tokens(conn) + print(f"агентский токен: {tok['agent']}\nклиентский токен: {tok['client']}\nбаза: {paths.db_path()}") + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100644 index 0000000..d1733a9 --- /dev/null +++ b/tests/test_cli.py @@ -0,0 +1,25 @@ +import json +from pathlib import Path + +from jira_hub import cli, db, paths, settings + +FIXTURE = Path(__file__).parent / "fixtures" / "config.example.json" + + +def test_init_import_tokens(tmp_path, monkeypatch, capsys): + monkeypatch.setenv("JIRA_HUB_HOME", str(tmp_path / "home")) + assert cli.main(["init", "--config", str(FIXTURE)]) == 0 + out = capsys.readouterr().out + assert "transitions_map" in out and "агентский токен" in out + assert cli.main(["init", "--config", str(FIXTURE)]) == 1 + assert "уже инициализирован" in capsys.readouterr().err + conn = db.connect(paths.db_path()) + assert settings.get(conn, "board_id") == 4216 + (tmp_path / "pkg").mkdir() + (tmp_path / "pkg" / "tasks.csv").write_text("Код,Статус по факту,Вид работы\nAURORA-1,В работе,UI\n", encoding="utf-8") + assert cli.main(["import", str(tmp_path / "pkg")]) == 0 + rep = json.loads(capsys.readouterr().out) + assert rep["notes"] == 1 and rep["missing_keys"] == ["AURORA-1"] + assert cli.main(["tokens"]) == 0 + assert settings.tokens(conn)["client"] in capsys.readouterr().out + conn.close()