Документация: модуль wiki и агенты модулей (#91)

Тест границы test_core_has_no_module_terms; ARCHITECTURE/CLAUDE/README; дельта-спека о хуках before_result/abort_result.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Andrey Epifancev 2026-10-24 03:37:17 +04:00
parent 11f42a3108
commit ede028346b
5 changed files with 127 additions and 26 deletions

View File

@ -1,12 +1,12 @@
# Архитектура
Хаб моста Jira: ядро + модули; сейчас один модуль — `jira`: SQLite-зеркало задач проектов-меток Jira-проекта AURORA, служебные заметки, очередь команд, HTTP API и MCP. Агент (`jira-agent`, виртуалка) опрашивает хаб, пишет в Jira и возвращает состояние; хаб Jira не вызывает.
Хаб моста Jira: ядро + два модуля. `jira` — SQLite-зеркало задач проектов-меток Jira-проекта AURORA, служебные заметки, очередь команд, HTTP API и MCP; `wiki` — Markdown-копия вики Eva по проектам, поиск и вложения по запросу. У каждого модуля свой агент на виртуалке (`jira-agent`, `eva-agent`): он опрашивает хаб, ходит в свою внешнюю систему и возвращает данные; хаб во внешние системы не ходит.
## Ядро и модули
Хаб — ядро (`src/jira_hub/core/`) и подключаемые модули (`src/jira_hub/modules/<имя>/`). Ядро держит механику: базу,
очередь команд, HTTP, настройки, реле, выпуск агента, каркас MCP и CLI. Модуль даёт смысл: схему своих таблиц, типы
команд, маршруты, настройки, MCP-инструменты, подкоманды. Сейчас модуль один — `jira`; следующие (wiki, sdlc) подключаются
команд, маршруты, настройки, MCP-инструменты, подкоманды. Сейчас модулей два — `jira` и `wiki`; следующие (sdlc) подключаются
так же, строкой в `core/registry.py`.
### Ядро (`core/`)
@ -17,19 +17,19 @@
| `errors` | ошибки, общие для ядра и модулей (`CommandError`, `HttpMapped`) | stdlib |
| `http` | общее для обработчиков HTTP: `ApiError`, `Raw`, разбор параметров, `mapped`, `not_found` | `errors`, `projects` |
| `modules` | контракт модуля: `Module` и `CommandHooks` (только структуры) | stdlib |
| `registry` | реестр модулей: ленивая сборка, `modules()`, `module(name)`, `module_of(тип_команды)`, проверка конфликтов (типы команд, маршруты, ключи настроек) | `modules`; модули — ленивым импортом внутри функции |
| `db` | `connect`, схема ядра (`setting`, `batch`, `command`, `project`, `agent_status`) и схемы модулей из `Module.schema`, миграции по `schema_version` (v6; копия базы перед миграцией; история шагов `MIGRATIONS`) | `backup`, `paths`; модули — только внутри шагов истории миграций (ленивый импорт) |
| `settings` | настройки (`setting`), токены, умолчания модулей (`ensure_defaults`) | `registry` (база — через `conn` вызывающего) |
| `projects` | проекты-метки: таблица `project`, проверка метки, создание, список (долг: `board_id`, `board_type`, `issues_count` — см. «Границы») | `db` |
| `registry` | реестр модулей: ленивая сборка, `modules()`, `module(name)`, `module_of(тип_команды)`, `agent_modules()`, `DEFAULT_AGENT`, проверка конфликтов (типы команд, маршруты, ключи настроек, поля проекта) | `modules`; модули — ленивым импортом внутри функции |
| `db` | `connect`, схема ядра (`setting`, `batch`, `command`, `project`, `agent_status`) и схемы модулей из `Module.schema`, миграции по `schema_version` (v7; копия базы перед миграцией; история шагов `MIGRATIONS`) | `backup`, `paths`; модули — только внутри шагов истории миграций (ленивый импорт) |
| `settings` | настройки (`setting`), токены (клиентский + по агенту на модуль: `setting.tokens = {client, agents: {<модуль>: <токен>}}`), умолчания модулей (`ensure_defaults`) | `registry` (база — через `conn` вызывающего) |
| `projects` | проекты-метки: таблица `project` (`label`, `name`, `created_at`), проверка метки, создание и правка (`PATCH`) с полями модулей, список = поля ядра + `project_view` модулей | `db`, `registry` |
| `queue` | механика очереди: пакеты, выдача (`claim`), отчёты, отмена, повтор, временные ключи `new:<n>`; смысл типов команд берёт у модуля через `CommandHooks` (`registry.module_of`) | `backup`, `db`, `registry`, `settings`, `errors` |
| `backup` | копии файла базы, ротация | `paths` |
| `files` | хранилище файлов `<данные>/files/<sha256>` (каталог — параметр, `Hub.files_dir`): `put`, `touch`, `sweep`, `sweep_downloads`, `path`/`exists`/`size`/`read` | `paths`, `registry` |
| `api` | HTTP-сервер, авторизация по токенам, фоновый `tick`, `Hub`; маршруты ядра `CORE_ROUTES` (`/agent/*`, `/files`, `/projects`, `/settings`, раздача `/app…`) и сводный `ROUTES` = ядро + маршруты модулей; уборка `files/` раз в сутки | `queue`, `projects`, `settings`, `backup`, `db`, `files`, `relay`, `release`, `registry`, `http`, `errors`, `paths` |
| `relay` | реле общей папки (`relay_dir`): файлы `req/` → свой HTTP → `resp/`; агент с `hub_url = folder:<путь>` ходит через него | `settings` |
| `release` | опубликованная версия агента `release/agent.py` + `manifest.json`: `publish` (только вверх), `current`, `load`, `hint` | `paths` |
| `release` | опубликованная версия агента модуля `release/<модуль>/agent.py` + `manifest.json`: `publish` (только вверх), `current`, `load`, `hint` | `paths` |
| `client` | HTTP-клиент хаба (`HubClient`, `from_home`, `get_bytes`) | `db`, `paths`, `settings` (только `from_home`) |
| `mcp` | каркас MCP: охрана («хаб выключен» — подсказка), привязка сессии к проекту (`PROJECT`), общие инструменты `projects` и `project_create`, `register_core_tools`, запуск сервера модуля по stdio | `client`, `registry` |
| `cli` | `init`, `serve`, `mcp [модуль] [--project]`, `tokens`, `agent publish` и подкоманды модулей (`Module.cli_register`/`cli_run`) | `api`, `mcp`, `registry`, `release`, `settings`, `db`, `paths` |
| `cli` | `init`, `serve`, `mcp [модуль] [--project]`, `tokens`, `agent publish [--module]` и подкоманды модулей (`Module.cli_register`/`cli_run`) | `api`, `mcp`, `registry`, `release`, `settings`, `db`, `paths` |
### Модуль `jira` (`modules/jira/`)
@ -51,12 +51,35 @@
| `mcp` | инструменты MCP: `issues`, `issue`, `batch_create`, `batch_*`, `comment`, `attachment`, `changes`, `refresh`, `settings_*` и др.; общие `projects`/`project_create` подключает `register_core_tools` | `core.mcp`, `core.client`, `commands`, `attachments` |
| `importer`, `cli` | разовый импорт CSV-пакета в заметки (`jira-hub import FOLDER`) | `mirror`, `core.db`, `core.settings` |
### Модуль `wiki` (`modules/wiki/`)
Точка входа — `modules/wiki/__init__.py` (`MODULE: Module`). Копию страниц хаб хранит файлами `wiki/<метка>/` в каталоге данных (Markdown + `_tree.json`), строки и FTS — в базе.
| Модуль | Ответственность | Зависит от |
|---|---|---|
| `schema` | таблицы модуля: `wiki_project`, `wiki_page`, `wiki_page_fts` (FTS5), `wiki_attachment`, `wiki_sync_stage` | stdlib |
| `settings` | умолчания (`wiki_pacing`, `wiki_auto_sync_hours`, `wiki_min_agent_version`), ключи для агента, проверка значений | `core.settings` |
| `paths` | каталог копий `<каталог базы>/wiki/<метка>/` | `core.db` |
| `projects` | поле проекта `wiki_root` (изменяемое), `project_view` (`wiki_root`, `pages_count`, `synced_at`…), `on_project_created`/`on_project_updated` (постановка `wiki_sync`, отмена `queued`, сброс копии при смене корня) | `core.projects`, `core.queue`, `commands`, `paths` |
| `commands` | типы `wiki_sync` и `wiki_fetch` (оба — чтение Eva, без хвоста и копии базы), `CommandHooks` модуля: проверки, паузы, `before_result`/`on_result`/`after_result`/`abort_result`, отбрасывание отчёта по сменённому корню | `core.queue`, `stage`, `sync`, `attachments` |
| `convert` | HTML Eva → Markdown (`markdownify`): замена ссылок по `ref` на `attachment:<id>` и `page:<код>`; HTML считается недоверенным | stdlib, `markdownify` |
| `stage` | приём пачек страниц от агента: проверка формы, перевод вне транзакции, стадия `wiki_sync_stage` | `core.db`, `convert` |
| `sync` | сборка копии из стадии в `wiki/.<метка>.new/` (`prepare`), запись строк и подмена каталога (`commit`), FTS | `core.db`, `paths`, `search`, `convert` |
| `search` | поиск по FTS5 (запасной LIKE по `index.md`, если FTS5 в SQLite нет), фрагменты | `paths` |
| `attachments` | вложения по запросу: состояние `none`/`fetching`/`ready`/`failed`, постановка `wiki_fetch`, хуки результата; адрес берётся только из `wiki_attachment` | `core.queue`, `core.files` |
| `tick` | фоновая синхронизация проектов с корнем раз в `wiki_auto_sync_hours` | `core.db`, `core.settings`, `commands`, `sync`, `search` |
| `api` | маршруты `/wiki/tree`, `/wiki/pages/{code}`, `/wiki/search`, `/wiki/sync`, `/wiki/status`, `/wiki/attachments/{id}[/fetch\|/content]` (клиент) и `POST /agent/wiki/pages` (агент `wiki`) | `core.http`, `core.queue`, `stage`, `attachments`, `search` |
| `mcp` | инструменты сервера `jira-hub-wiki`: `projects`, `project_create`, `root_set`, `tree`, `page`, `search`, `attachment`, `sync`, `status`; Markdown и имена вложений недоверенные | `core.mcp`, `core.client` |
### Контракт модуля
`core/modules.py` задаёт две структуры; ядро знает только их.
- `Module(name, schema, settings_defaults, agent_keys, validate_setting, settings_view, on_init, commands, routes, tick, file_refs, on_project_created, mcp_server, cli_register, cli_run)` — всё необязательное, кроме `name`.
- `CommandHooks` — что модуль знает о своих типах команд: `types`, `read_types` (не пишут во внешнюю систему: без хвоста и копии базы), `validate`, `check_batch`, `tail`, `pause`, `deliver_params`, `temp_definer`, `temp_keys_used`, `substitute`, `drop_unresolved`, `retry_params`, `check_confirmed`, `on_claim`, `on_failed`, `check_result`, `on_result`, `after_result`, `on_late_result`. Очередь вызывает их по типу команды через `registry.module_of`.
- `Module(name, schema, settings_defaults, agent_keys, agent_min_version_key, validate_setting, settings_view, on_init, commands, routes, tick, file_refs, project_fields, project_view, on_project_created, on_project_updated, mcp_server, cli_register, cli_run)` — всё необязательное, кроме `name`. Модуль с `commands` — «вид агента»: у него свой агент, токен, `agent_status` и релиз.
- Поля проекта: ядро знает только `label`, `name`, `created_at`. Остальное объявляет модуль: `ProjectField(name, validate, editable)`; значения для `GET /projects` (в т.ч. вычисляемые) отдаёт `project_view(conn, labels)`; `on_project_created(conn, project, fields, author)` и `on_project_updated(...)` пишут свои поля и возвращают пакеты. `POST /projects` принимает поля всех модулей (неизвестное — 422), `PATCH /projects/{label}` — только изменяемые (`editable`). `registry.check` падает при совпадении имени поля у двух модулей или с полем ядра.
- `CommandHooks` — что модуль знает о своих типах команд: `types`, `read_types` (не пишут во внешнюю систему: без хвоста и копии базы), `validate`, `check_batch`, `tail`, `pause`, `deliver_params`, `temp_definer`, `temp_keys_used`, `substitute`, `drop_unresolved`, `retry_params`, `check_confirmed`, `on_claim`, `on_failed`, `check_result`, `on_result`, `after_result`, `on_late_result` и два необязательных — `before_result`, `abort_result`. Очередь вызывает их по типу команды через `registry.module_of`.
- `before_result(conn, cmd, status, result, files_dir)` ядро вызывает в `report_result` до транзакции отчёта, только для выданной (`claimed`) команды. Нужен, чтобы тяжёлая подготовка (сборка копии вики, запись файлов) шла вне `BEGIN IMMEDIATE` и не держала блокировку базы, в которой ждут `claim` другого агента и веб (рулинг R-6). `ValueError` из него ядро превращает в `RuntimeError` (500, агент повторит отчёт), чтобы не спутать с неверным статусом отчёта.
- `abort_result(conn, cmd)` ядро вызывает, если отчёт не дошёл до `after_result` (поздний или идемпотентный выход, исключение в транзакции): модуль снимает подготовку `before_result` и её блокировку. При успехе подготовку снимает `after_result`.
`registry.check` при сборке падает на конфликте: один тип команды у двух модулей, пересечение маршрутов или ключей настроек. Уникальность имён модулей проверяется только в вебе (`web/src/modules/modules.test.tsx`); в Python реестр её не проверяет.
@ -69,6 +92,11 @@
`web/src/modules/types.ts` (`pages`, `nav`, `Provider`, `SidebarStatus`, `TopbarNotice`, `useOverlay`, `useOnProjectCreated`);
список модулей — константа `MODULES` в `web/src/modules/index.ts` (хуки вызываются в постоянном порядке).
Модуль `wiki` (`web/src/modules/wiki/index.tsx`): страница «Вики» (`/app/wiki`): дерево (`Tree`) с поиском (`Search`), страница
(`WikiPage`, `PageView` — `react-markdown` + `remark-gfm`, без `rehype-raw`; ссылки `page:` ведут на страницу вики, `attachment:`
скачивают файл через `download.ts`, `urlTransform.ts` пропускает только эти схемы и http(s)), форма корня без привязки (`RootForm`),
плашка синхронизации в боковой панели (`SyncStatus`); поле «Корень вики» в окне «Новый проект» — через `projectFields`.
Модуль `jira` (`web/src/modules/jira/index.tsx`): страницы «Доска связей» (`/app/board`), «Задачи» (`/app/issues`), «Лента»
(`/app/feed`), карточка `/app/issues/{key}` окном поверх страницы (`issues/overlay.tsx`, `IssueModal.tsx`; оболочка рисует
страницу по `location.state.background`). Внутри: `board/` (чистые `model`, `analysis`, `layout`, `storage.ts` — ключи
@ -84,8 +112,9 @@ golden-эталоны `__fixtures__/`), `issues/` (список, карточк
Цепочки зависимостей (Python):
- ядро: `api → queue → settings/projects/files/registry → db → backup → paths`; `api → relay → settings`; `api → release → paths`; `files → paths`; `cli → api/mcp/registry`; `mcp → client → HTTP API`; `registry → (лениво) modules.*`;
- модуль wiki: `commands → stage/sync/attachments → convert/search/paths → core.*`; `projects → commands/paths`; `api → stage/attachments/search → core.http/core.queue`; `tick → commands/sync`;
- модуль jira: `hooks → commands/mirror/history/refresh/attachments/fetch → core.*`; `mirror → history → projects`; `mirror → attachments → core.files`; `api → mirror/history/fetch/refresh → core.http/core.queue`; `importer → mirror`;
- веб: `shell → modules/index → modules/jira → ui/api/projects` (модуль не импортирует оболочку, кроме типов контракта).
- веб: `shell → modules/index → modules/jira, modules/wiki → ui/api/projects` (модуль не импортирует оболочку, кроме типов контракта).
## Потоки данных
- Запись в Jira: клиент (MCP/интерфейс) → `POST /batches` → `queue` → агент `claim` → Jira → `POST /agent/commands/{id}/result`.
@ -97,24 +126,30 @@ golden-эталоны `__fixtures__/`), `issues/` (список, карточк
агент v13 после состояния читает changelog Jira → `POST /agent/history` → `history.record_changes` (повторы
отбрасываются по `(history_id, seq)`) → отчёт `refresh` с `result.history` → `sync_run.history_at` у полного прогона
(новая отметка) → `GET /changes`, `GET /issues/{key}.changes`, MCP `changes`.
- Синхронизация вики: `wiki_sync {label, root}` ставят MCP `sync`, кнопка «Обновить» (`POST /wiki/sync`), привязка/смена корня (`on_project_created`/`on_project_updated`) и `tick` (раз в `wiki_auto_sync_hours`) → `claim` агента `wiki` → eva-agent обходит дерево и шлёт пачки `POST /agent/wiki/pages` (≤50 страниц, ≤4 МБ) → `stage.accept` переводит HTML в Markdown вне транзакции и кладёт в `wiki_sync_stage` → отчёт `done {root_title, codes}` → `before_result` (`sync.prepare`: сборка `wiki/.<метка>.new/`, выделение id вложений, под per-label `Lock`) → транзакция отчёта (`on_result` = `sync.commit`: строки `wiki_page`/`wiki_attachment`/FTS, атомарная подмена каталога) → `after_result` снимает подготовку; при сбое — `abort_result`. Отчёт по команде с устаревшим корнем отбрасывается (`result.discarded`), копия прежняя.
- Вложение вики: `POST /wiki/attachments/{id}/fetch` (веб, MCP `attachment`) → `attachments.request_fetch` (одна транзакция: `state = fetching` + пакет `wiki_fetch {attachment_id, url}`) → `claim` → eva-agent качает файл только с хоста Eva → `POST /agent/files` → результат `{file, size}` → хаб проверяет файл в `files/` и ставит `ready` (иначе `failed`) → `GET /wiki/attachments/{id}/content`; в MCP копия `downloads/wiki-<id>/<имя>`.
- Агент за VPN: агент → файлы в общей папке (`relay_dir`) → `relay` → HTTP хаба → ответ файлом.
- Самообновление агента: `jira-hub agent publish` → `release/` → `claim.agent_release` → агент скачивает `GET /agent/release`; состояние обновления агент шлёт в `claim`, хаб держит в памяти (вместе с признаком `agent_reported` — был ли `claim` с запуска хаба) и отдаёт в `GET /agent`.
- Токены: агентский — только `/agent/*`, клиентский — остальное; MCP берёт клиентский токен из базы через `HubClient.from_home`.
- Самообновление агента: `jira-hub agent publish [--module M] PATH` → `release/<модуль>/` → `claim.agent_release` → агент скачивает `GET /agent/release`; состояние обновления агент шлёт в `claim`, хаб держит в памяти (вместе с признаком `agent_reported` — был ли `claim` с запуска хаба) и отдаёт в `GET /agent`.
- Токены: клиентский — всё, кроме `/agent/*`; по токену на каждый модуль-агент (`jira`, `wiki`) — только `/agent/*`, причём маршруты `scope=agent` модуля пускают лишь его агента (токен чужого — 401); `claim` выдаёт агенту только команды его модуля, по одной в полёте на агента; `agent_status` (ключ — имя агента), релиз `release/<модуль>/` и самообновление — по агентам; MCP берёт клиентский токен из базы через `HubClient.from_home`. Токен Eva в хабе не хранится — только в `agent.json` eva-agent.
## Границы
- Ядро (`core/`) не импортирует модули (`jira_hub.modules.*`): исключения — `core/registry.py` (ленивый импорт внутри функции) и шаги истории миграций в `core/db.py` (ленивый импорт внутри шага). Проверяет `tests/test_registry.py::test_core_imports`.
- Типы команд (`ensure_*`, `refresh`, `fetch_attachment` и др.) и термины jira (`sync_run`, `issue_attachment`, `attachment_file`, `issue_comment`, `jql`) живут только в модулях; ядро знает их лишь через `CommandHooks`. Проверяет `test_core_has_no_jira_terms` (исключения — `core/db.py`, история миграций, и строки с меткой `долг hub-modules`).
- Новые имена в ядре — без «jira». Модуль не вызывается из ядра иначе как через `registry` и контракт `Module`.
- Долг проектов (change wiki): проект-метка пока устроен под jira — `board_id`, `board_type` и проверка `board_id` в `core/projects.py`, `issues_count` в списке проектов и `projects.create(..., board_id)` в HTTP-слое (`core/api.py`: `h_projects_get`, `h_projects_post`), `project_create.board_id` и `fmt_projects` в `core/mcp.py`, поле доски в `web/src/projects/NewProjectDialog.tsx`. Места помечены комментарием `долг hub-modules: проекты → модуль jira (change wiki)`; убирать их можно только вместе с переносом проектов в модуль.
- Типы команд (`ensure_*`, `refresh`, `fetch_attachment`, `wiki_sync`, `wiki_fetch` и др.) и термины модулей (jira: `sync_run`, `issue_attachment`, `attachment_file`, `issue_comment`, `jql`, поля досок; wiki: `eva`, `wiki_`, `wiki/`, `DOC-`) живут только в модулях; ядро знает их лишь через `CommandHooks` и поля проекта. Проверяют `test_core_has_no_jira_terms` (исключение — `core/db.py`, история миграций) и `test_core_has_no_module_terms` (исключение — `core/registry.py`). Меток-исключений «долг» нет.
- Новые имена в ядре — без названий модулей. Модуль не вызывается из ядра иначе как через `registry` и контракт `Module`.
- Копию вики (`wiki/<метка>/`, `wiki_page`, FTS) пишет только `sync.commit` в отчёте `wiki_sync`; MCP и API её читают. Модуль wiki не обращается к Eva и не хранит токен Eva.
- Зеркало (включая `issue_attachment`) пишет только `mirror.apply_state` по запросу агента (и заметки через `PUT /issues/{key}/note`); из очереди и API задач напрямую не правят.
- Журнал `issue_change` пишет только `history.record_changes` по `POST /agent/history`; API и MCP его только читают.
- Хаб не знает пароль Jira и не обращается к Jira; `init` не переносит `auth`, `base_url`, `verify_ssl`, `ca_bundle`.
- `queue` не создаёт команд сам: повтор после failed — новый пакет. Выданная без отчёта команда довыдаётся при следующем `claim` (до 3 выдач, затем failed).
- MCP (`core/mcp`, `modules/jira/mcp`) не импортирует `queue`/`db`/`projects` и модульные `mirror`/`history` напрямую, ходит только через `client`; из `core.files` берёт только предел `MAX_SIZE`, из `attachments` — только `fmt_size`; `downloads/` пишет только он (хаб эту папку лишь убирает).
- MCP (`core/mcp`, `modules/*/mcp`) не импортирует `queue`/`db`/`projects` и модульные `mirror`/`history` напрямую, ходит только через `client`; из `core.files` берёт только предел `MAX_SIZE`, из `attachments` — только `fmt_size`; `downloads/` пишет только он (jira — `<id>/`, wiki — `wiki-<id>/`) (хаб эту папку лишь убирает).
- `commands`, `mirror`, `queue` не знают про HTTP; HTTP живёт только в `core/api`, `modules/*/api`, `client` и `relay` (реле — клиент своего же HTTP).
- Веб-приложение (`web/`) ходит в хаб только через HTTP API (`web/src/api/client.ts`); словари `kind_labels`, `link_types` и набор закрытых статусов `closed_statuses` берёт из `GET /settings` и своих копий не держит. Модуль веба не импортирует другой модуль; общее — в `shell`, `ui`, `api`, `projects`.
## Открытые вопросы
- Правки из UI в Jira (спринт, срок, связи, исполнитель) — change `viewer-edit`; страницы очереди, агента и настроек — позже.
- Автоподстройка темпа по фактам 429 не реализована: хаб только показывает их.
- Запись в вики Eva (правка страниц из хаба) не делается: модуль только читает.
- История страниц вики и частичная перекачка: сейчас каждая синхронизация перекачивает всё дерево, копия без истории.
- Переименование проекта-метки и перенос корня вики между проектами не предусмотрены (меняется только корень — `PATCH /projects`).
- Ручной просмотр вёрстки веб-страницы «Вики» на живых данных (таблицы, вложенные списки) — за владельцем после выкатки.
- Переименование хаба в sdlc-hub (модули jira + wiki, eva-agent) — после перестройки кода; имена пакета, команды и MCP-сервера пока `jira-hub`.

View File

@ -1,7 +1,7 @@
# jira-hub
## Что это
Хаб моста Jira для АРМ КА: единственный источник истины по задачам проектов-меток Jira-проекта AURORA на Mac mini (`192.168.0.106`). Хранит зеркало задач, служебные заметки и очередь команд в SQLite, отдаёт HTTP API в локальной сети (агенту на виртуалке `jira-agent` и интерфейсам) и MCP-сервер для Claude. Главный сценарий: из чата поставить пакет команд, агент на виртуалке выполняет их в Jira по правилу «проверь, потом сделай» и возвращает состояние задач в зеркало. Спеки: `openspec/specs/`; активные change — `openspec/changes/` (кроме `archive/`).
Хаб моста Jira для АРМ КА: единственный источник истины по задачам проектов-меток Jira-проекта AURORA на Mac mini (`192.168.0.106`) и Markdown-копии вики Eva по проектам (модуль `wiki`). Хранит зеркало задач, служебные заметки и очередь команд в SQLite, отдаёт HTTP API в локальной сети (агенту на виртуалке `jira-agent` и интерфейсам) и MCP-сервер для Claude. Главный сценарий: из чата поставить пакет команд, агент на виртуалке выполняет их в Jira по правилу «проверь, потом сделай» и возвращает состояние задач в зеркало. Спеки: `openspec/specs/`; активные change — `openspec/changes/` (кроме `archive/`).
## Стек и ограничения
- Веб-приложение — `web/` (React 19 + TypeScript + Vite, TanStack Query, Vitest, Playwright; Node 24, npm); сборка в `src/jira_hub/web/app/` не в git.
@ -9,15 +9,16 @@
- Хаб не знает пароль Jira и не ходит в Jira: учётные данные и сеть до Jira только у агента.
- Зеркало задач пишет только `mirror.apply_state` по загрузке состояния от агента (`POST /agent/state`); правки идут командами через очередь.
- Хаб не создаёт команды сам: повтор после failed — новая команда по запросу человека/интерфейса. Команду, выданную агенту и оставшуюся без отчёта, хаб довыдаёт при следующем claim (до 3 выдач, затем failed).
- Данные: `~/.local/share/jira-hub/` (`JIRA_HUB_HOME`): `hub.sqlite` (зеркало, очередь, журнал изменений `issue_change`), `backups/`, `release/` (опубликованная версия агента), `files/` (файлы вложений по sha256, уборка через 30 дней), `downloads/` (копии вложений для MCP `<id>/<имя>`, уборка 30 дней); каталог данных, `backups/`, `release/`, `files/` и `downloads/` — права 0700. Порт по умолчанию 8790.
- Два статических токена (в таблице `setting`): агентский открывает только `/agent/*`, клиентский — остальное.
- Данные: `~/.local/share/jira-hub/` (`JIRA_HUB_HOME`): `hub.sqlite` (зеркало, очередь, журнал изменений `issue_change`), `backups/`, `release/` (опубликованная версия агента), `files/` (файлы вложений по sha256, уборка через 30 дней), `downloads/` (копии вложений для MCP `<id>/<имя>`, у wiki `wiki-<id>/<имя>`, уборка 30 дней), `wiki/<метка>/` (Markdown-копия вики проекта: `index.md` в каталоге страницы, `_tree.json`); каталог данных, `backups/`, `release/<модуль>/`, `files/`, `downloads/` и `wiki/` — права 0700. Порт по умолчанию 8790.
- Статические токены (в таблице `setting`): клиентский — всё, кроме `/agent/*`; по одному токену агента на модуль (`jira`, `wiki`) — только `/agent/*` и только маршруты своего модуля. Токен Eva в хабе не хранится.
## Архитектура
- `src/jira_hub/core/` — ядро: `db`, `settings`, `projects`, `queue` (механика очереди), `files`, `api` (HTTP, `CORE_ROUTES` + маршруты модулей в `ROUTES`, фоновый `tick`), `relay`, `release`, `client`, `mcp` (каркас MCP), `cli`; контракт модуля `Module`/`CommandHooks` — `core/modules.py`, реестр — `core/registry.py`.
- `src/jira_hub/modules/jira/` — модуль jira: `schema`, `mirror`, `history`, `attachments`, `fetch`, `commands`, `refresh`, `projects`, `hooks` (смысл типов команд для очереди), `api`, `mcp`, `settings`, `importer`/`cli`. Входная точка — `MODULE` в `modules/jira/__init__.py`.
- Граница: ядро не импортирует модули (кроме ленивого импорта в `registry.py` и шагах миграций `db.py`), типы команд и термины jira живут в модуле, новые имена в ядре — без «jira»; проверяет `tests/test_registry.py`. Места долга проектов (`board_id`, `board_type`, `issues_count`) помечены `долг hub-modules`.
- `src/jira_hub/modules/wiki/` — модуль wiki: `schema`, `settings`, `paths`, `projects` (поле `wiki_root`), `commands` (`wiki_sync`, `wiki_fetch` и хуки очереди), `convert` (HTML → Markdown), `stage`, `sync`, `search` (FTS5), `attachments`, `tick`, `api`, `mcp`. Входная точка — `MODULE` в `modules/wiki/__init__.py`.
- Граница: ядро не импортирует модули (кроме ленивого импорта в `registry.py` и шагах миграций `db.py`), типы команд и термины модулей (jira, wiki, eva) живут в модуле, новые имена в ядре — без названий модулей; проверяют `test_core_imports`, `test_core_has_no_jira_terms`, `test_core_has_no_module_terms` в `tests/test_registry.py`. Меток-исключений нет.
- Цепочка ядра: `db` ← `settings`, `projects` ← `queue` ← `api` ← `cli`; `files` ← `queue`, `api`; реле `relay` — поток в `serve`; `backup` вызывается из `api` и из `db`. Модуль jira: `hooks → commands/mirror/history/refresh/attachments/fetch → core`.
- Веб: оболочка и общее — `web/src/{shell,ui,api,projects,auth,styles}`, модули — `web/src/modules/<имя>/` (контракт `HubModule` в `modules/types.ts`, список — `modules/index.ts`); модуль jira — страницы доски, задач и ленты.
- Веб: оболочка и общее — `web/src/{shell,ui,api,projects,auth,styles}`, модули — `web/src/modules/<имя>/` (контракт `HubModule` в `modules/types.ts`, список — `modules/index.ts`); модуль jira — страницы доски, задач и ленты; модуль wiki (`web/src/modules/wiki/`) — страница «Вики» (`/app/wiki?project=<метка>`).
- Подробная карта модулей и границ — `ARCHITECTURE.md`.
## Команды
@ -27,8 +28,8 @@
- `uv run jira-hub serve [--host] [--port]` — локальный запуск хаба.
- Веб-приложение — `http://127.0.0.1:8790/app/` (доска — `/app/board?project=<метка>`, задачи — `/app/issues?project=<метка>`, лента изменений — `/app/feed?project=<метка>`; вход по клиентскому токену); `/` и `/links` перенаправляют на доску.
- `make web` — сборка приложения (после `git pull` на Mac mini, до перезапуска хаба); `make check` — pytest + lint и тесты фронта; `cd web && npm run dev` — разработка с прокси на тестовый хаб `bash web/scripts/test-hub.sh` (:8791); `cd web && npm run e2e` — Playwright; `cd web && npm run golden` — перегенерировать эталоны доски из `links.html@3931a88` (только при смене фикстур).
- `uv run jira-hub import FOLDER` — импорт CSV-пакета `admin/jira-sync`; `uv run jira-hub mcp [модуль] [--project <метка>]` — MCP модуля по stdio (по умолчанию `jira`; с привязкой сессии к проекту); `uv run jira-hub tokens` — показать токены.
- `uv run jira-hub agent publish PATH` — опубликовать версию агента для самообновления (только вверх).
- `uv run jira-hub import FOLDER` — импорт CSV-пакета `admin/jira-sync`; `uv run jira-hub mcp [модуль] [--project <метка>]` — MCP модуля по stdio (по умолчанию `jira`; `jira-hub mcp wiki` — сервер `jira-hub-wiki`; с привязкой сессии к проекту); `uv run jira-hub tokens` — показать токены (клиентский и по агенту на модуль).
- `uv run jira-hub agent publish [--module jira|wiki] PATH` — опубликовать версию агента модуля для самообновления (только вверх; по умолчанию `jira`; публикация лежит в `release/<модуль>/`).
## Тесты
- Golden-тесты: `tests/fixtures/<name>.*` + эталон `<name>.expected.*`. Внешние сервисы в тестах мокаются.

View File

@ -28,7 +28,7 @@ uv run jira-hub serve # 0.0.0.0:8790
```
`init` переносит в настройки хаба всё, что относится к проекту Jira (поля, типы, карту переходов, типы связей, доску,
приоритеты), и создаёт два токена: агентский и клиентский. Адрес и учётные данные Jira (`base_url`, `auth`, `verify_ssl`,
приоритеты), и создаёт токены: клиентский и по одному на агента каждого модуля (`jira`, `wiki`). Адрес и учётные данные Jira (`base_url`, `auth`, `verify_ssl`,
`ca_bundle`) остаются у агента и в хаб не попадают. Повторный `init` отказывает ("уже инициализирован"); с `--force`
настройки переписываются. Токены можно показать позже: `uv run jira-hub tokens`.
@ -61,6 +61,22 @@ claude mcp add -s user jira-hub -- uv run --project /Users/andrey.epifancev/proj
После перезапуска хаба или обновления кода MCP — `/mcp` → reconnect.
MCP вики — отдельный сервер `jira-hub-wiki` (`jira-hub mcp wiki`). Регистрация в `.mcp.json` репозитория проекта (рядом с `jira-hub`, с привязкой к проекту):
```json
{
"mcpServers": {
"jira-hub-wiki": {
"command": "uv",
"args": ["run", "--project", "/Users/andrey.epifancev/projects/jira-bridge/jira-hub", "jira-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` репозитория проекта
(метка сравнивается точно, с учётом регистра):
@ -113,6 +129,23 @@ claude mcp add -s user jira-hub -- uv run --project /Users/andrey.epifancev/proj
{"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 jira-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 jira-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/jira-hub/wiki/<метка>/` (Markdown, по каталогу на страницу). Настройки модуля: `wiki_pacing` (пауза агента между
запросами к Eva, с), `wiki_auto_sync_hours`, `wiki_min_agent_version`.
## Транспорт через общую папку
Если сеть между виртуалкой и Mac mini закрыта (VPN «Континент ZTN» на виртуалке режет локальную сеть), агент ходит
@ -143,7 +176,8 @@ curl -s -X PUT -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/j
Опубликовать новую версию:
```bash
uv run jira-hub agent publish ../jira-agent/src/agent.py
uv run jira-hub agent publish ../jira-agent/src/agent.py # модуль jira (по умолчанию)
uv run jira-hub agent publish --module wiki ../eva-agent/src/agent.py
```
Версия берётся из строки `AGENT_VERSION = N` и должна быть больше опубликованной; файл и `manifest.json` ложатся в

View File

@ -44,3 +44,18 @@ jira-agent, `wiki` — eva-agent). Агент SHALL определяться п
#### Scenario: Поля jira прежние
- **WHEN** после миграции v7 вызван `GET /projects`
- **THEN** у `AWPCA` `board_id = 4216`, `board_type = scrum`, `issues_count` — как до миграции
### Requirement: Подготовка отчёта команды модулем
Контракт `CommandHooks` SHALL содержать два необязательных хука. `before_result(conn, cmd, status, result, files_dir)`
SHALL вызываться ядром при отчёте по выданной команде до транзакции отчёта, чтобы модуль готовил тяжёлую работу (перевод,
запись файлов) вне блокировки базы; `ValueError` из него SHALL отдаваться как ошибка сервера, а не как неверный статус
отчёта. `abort_result(conn, cmd)` SHALL вызываться, когда отчёт не дошёл до `after_result` (ранний выход, исключение), и
снимать подготовку `before_result`. Модуль без этих хуков SHALL работать как прежде.
#### Scenario: Подготовка до транзакции
- **WHEN** агент шлёт отчёт `done` по команде типа модуля с `before_result`
- **THEN** `before_result` вызван до `BEGIN IMMEDIATE`, после фиксации транзакции — `after_result`
#### Scenario: Отчёт прерван
- **WHEN** `before_result` выполнен, а транзакция отчёта завершилась исключением
- **THEN** вызван `abort_result`, подготовка снята, команда остаётся выданной

View File

@ -115,3 +115,19 @@ def test_core_has_no_jira_terms():
continue
hits.append(f"{path.name}:{i + 1}: {line.strip()}")
assert hits == [], "термины jira в ядре:\n" + "\n".join(hits)
# термины модуля wiki/eva: ядро их не знает (рулинг R-3)
MODULE_TERMS = re.compile(r"\beva\b|\beva-agent\b|wiki_|wiki/|DOC-", re.IGNORECASE)
def test_core_has_no_module_terms():
"""Ядро не знает терминов модуля wiki и eva; исключение — registry.py (ленивый импорт модулей)."""
hits = []
for path in sorted(CORE.glob("*.py")):
if path.name == "registry.py":
continue
for i, line in enumerate(path.read_text(encoding="utf-8").splitlines()):
if MODULE_TERMS.search(line):
hits.append(f"{path.name}:{i + 1}: {line.strip()}")
assert hits == [], "термины модулей в ядре:\n" + "\n".join(hits)