69 KiB
Context
Хаб после #87 (main f7add9b) — ядро jira_hub/core/ и модуль jira_hub/modules/jira/, схема v6. Контракт модуля —
core/modules.py (Module, CommandHooks), реестр — core/registry.py (сейчас [jira.MODULE]).
Что в ядре зашито под один агент и под Jira (карта кода 24.10.2026):
- Токены. Один агентский и один клиентский токен в
setting.tokens = {agent, client}(core/settings.py:71-76).Handler._auth(core/api.py:406-415) пускает на любой маршрут со scopeagentлюбой агентский токен. agent_status— одна строкаid = 1(core/db.py:52-59). Её пишут claim, отчёт и отмена (core/queue.py, 7 мест), читаютagent_version()иagent_view().claim(core/queue.py:227-271) берёт любую команду очереди: SQL_nextи_redeliverне фильтруют ни по типу, ни по агенту. «Одна в полёте» тоже общая.- Состояние самообновления держится в памяти хаба одним объектом
Hub.agent_update(core/api.py:47, 91-104). - Релиз один:
release/agent.pyиmanifest.json(core/release.py,paths.release_dir()), командаjira-hub agent publish PATH. - Настройки агента.
/agent/settingsотдаёт ключи всех модулей и ключи ядраmin_agent_version,settings_version(core/settings.py:19, 36-41). Ключboard_idвAGENT_KEYSjira мёртвый. - Реле (
core/relay.py) про агентов не знает: оно пересылает метод, путь иAuthorizationв свой HTTP. Имена файлов запросов (uuid4().hex) задаёт агент. Два агента в одной папкеrelay_dirне мешают друг другу. - Проекты. Колонки
board_id,board_typeлежат в таблице ядраproject.issues_countсчитается в ядре по таблице модуляissue. Поле доски жёстко вшито вPOST /projects, MCPproject_create/projectsи окно «Новый проект». Модуль может только поставить пакет при создании проекта (on_project_created). Все такие места помеченыдолг hub-modules(решение 12 hub-modules). - MCP
agent_status— инструмент модуля jira.GET /agentотдаёт один объект. - Ориентир для модуля wiki — модуль jira:
modules/jira/fetch.py(скачивание вложения по запросу командойfetch_attachment),refresh.maybe_auto_refresh(фоновый пакет чтения),core/files.py(хранилище по sha256,MAX_SIZE10 МБ, уборка поfile_refs).
Eva. evaTeam, JSON-RPC 2.2 по адресу <eva>/api/?m=<метод>, заголовок Authorization: Bearer <токен>, тело
{jsonrpc: "2.2", method, callid, kwargs}. Используются методы:
CmfDocument.get(filter=[["code","==",<код>]], fields=[…]);CmfDocument.list(filter, fields, slice=[from,to]), страницы по 200.
Поля списка: id, code, name, parent, tree_parent, project, orderno. Текст страницы — HTML в поле text.
Дерево строится локально по parent/tree_parent из списка всех документов проекта Eva. Фильтр parent.code API не
принимает (-32001). Если у страницы нет project, дерево обходится в ширину через фильтр tree_parent.code.
Картинки и файлы — ссылки <img src>/<a href> на тот же хост с путём /(files?|attachments?|download|media|upload).
Всё это обкатано в eva_wiki_export.py (корень workspace).
Реальные форматы ссылок взяты из выгрузки ТЗ АРМ КА (awp-ca/docs/source/tz/, корень DOC-019855, 22 страницы, 388 КБ
Markdown; сырого HTML в выгрузке нет):
- Ссылка на страницу — относительный адрес без хоста и без ведущей
/:project/Document/DOC-019834. - Картинка —
/files/obj/CmfDocument/CmfDocument%3A<3 символа>/CmfDocument%3A<uuid>/<имя в URL-кодировке>?version=1. - Файл, прикреплённый к странице (xlsx, dbml), выглядит как миниатюра
…/<имя файла>.meta/thumbnail.jpg?version=1, значокstatic/attach-file.png, имя файла и дата текстом. Ссылка на сам файл в выгрузке не сохранилась: её, вероятно, съел markdownify. Адрес файла — адрес миниатюры без.meta/thumbnail.jpg. static/…— ресурсы интерфейса Eva, не вложения.
Объём настоящего дерева невелик (десятки страниц), но решения ниже рассчитаны и на сотни. Есть ли у документа дата изменения и история версий, неизвестно: публичной документации API нет, Eva доступна только из сети банка.
Владелец (brainstorming 23–24.10.2026): см. proposal, «Why».
Goals / Non-Goals
Goals:
- проект хаба получает корень Eva, дерево его страниц лежит у хаба Markdown-копией;
- Claude ищет и читает вики проекта через отдельный MCP-сервер;
- веб показывает дерево и страницы проекта;
- eva-agent — второй агент; хаб различает агентов;
- в ядре не остаётся Jira (метки
долг hub-modulesсняты).
Non-Goals: см. proposal. Сверх них — правка уже скачанной копии руками: копия перезаписывается синхронизацией.
Decisions
1. Вид агента = модуль с командами
У каждого модуля, у которого есть commands, свой агент. Имя вида совпадает с именем модуля: агент jira —
jira-agent, агент wiki — eva-agent. Отдельного справочника видов нет.
Почему: модуль уже владеет своими типами команд, настройками для агента и маршрутами /agent/<…> своего модуля.
Агент без модуля бессмыслен, модуль с командами без агента тоже.
2. Токен агента определяет его вид
setting.tokens становится {client, agents: {<модуль>: <токен>}}. Миграция v7 переносит прежний tokens.agent в
tokens.agents.jira, поэтому jira-agent v14 работает со своим прежним токеном. ensure_defaults создаёт недостающий
токен для каждого модуля с командами. jira-hub tokens печатает клиентский токен и токены агентов с именем модуля.
Handler._auth("agent") находит модуль по токену (hmac.compare_digest с каждым) и кладёт его в контекст запроса.
Дальше:
- маршруты ядра
/agent/*работают от имени этого агента; - маршруты модуля со scope
agent(у jira это/agent/state,/agent/history,/agent/known-keys) пускают только агента своего модуля, токен другого агента получает 401.
Клиентский токен по-прежнему один.
3. agent_status по агентам
Миграция v7 пересоздаёт таблицу с ключом agent TEXT PRIMARY KEY и переносит строку id = 1 в agent = 'jira'.
Колонку last_jira_call_at переименовываем в last_call_at. Протокол claim (поле last_jira_call_at в теле от
jira-agent) не меняется: хаб читает и last_call_at, и last_jira_call_at.
Строка агента появляется при старте хаба, по одной на модуль с командами. Все места WHERE id = 1 становятся
WHERE agent = ?. queue.agent_version(conn) превращается в agent_version(conn, agent). Модуль jira в fetch.py
спрашивает версию агента jira.
4. claim выдаёт только команды своего модуля
queue.claim(conn, agent, …). _next и _redeliver берут только команды, у которых type входит в
CommandHooks.types модуля agent. Пакет и так однородный: «команды разных модулей в одном пакете» отвергаются с
#87.
«Одна команда в полёте» считается на агента. Агенты работают параллельно: долгая синхронизация вики не задерживает
ensure_* в Jira. current_command_id хранится в строке своего агента. Копия базы перед первой пишущей командой
пакета остаётся как есть: команды wiki в этом change только читают (read_types).
5. Самообновление по агентам
- Релиз:
release/<модуль>/agent.pyиmanifest.json. При старте хаб один раз переносит прежниеrelease/agent.pyиrelease/manifest.jsonвrelease/jira/. Переносит, только еслиrelease/jira/ещё нет; ошибка переноса пишется в лог, хаб стартует. - Публикация:
jira-hub agent publish [--module <модуль>] PATH, по умолчаниюjira. Неизвестный модуль или модуль без команд — код 2, как уmcp. Проверки и вывод прежние. - Ответы агенту:
claimиGET /agent/releaseотдают релиз вида, к которому относится токен. - Память хаба:
Hub.agent_updateстановится словарём по агентам. GET /agent?module=<модуль>, без параметра —jira, как сейчас. ДобавляетсяGET /agents— список всех агентов в том же формате с полемmodule.- MCP:
agent_statusостаётся у jira, у wiki есть свой (решение 12).
6. Настройки агента по модулю
GET /agent/settings отдаёт ключи ядра и agent_keys только своего модуля: jira-agent не видит ключей вики и
наоборот. settings_version остаётся общим счётчиком. Лишний перезапрос настроек у eva-agent после правки настроек
jira безвреден.
min_agent_version переезжает из ядра в модуль jira: имя ключа и значение прежние, для jira-agent ничего не меняется.
Модуль задаёт ключ своей минимальной версии полем Module.agent_min_version_key; у wiki это wiki_min_agent_version,
по умолчанию 1. agent_view(conn, agent) берёт минимальную версию по этому ключу. Мёртвый ключ board_id из
AGENT_KEYS jira удаляется.
Требование для jira-agent v14: ответ /agent/settings для токена jira совпадает с нынешним, кроме ключа board_id.
Сейчас он всегда null, агент его не читает (проверить по jira-agent/src/agent.py при реализации; если читает —
ключ оставить со значением null).
7. Реле не меняется
Одна общая папка relay_dir на обоих агентов. Запросы различаются токеном, имена файлов — uuid4().hex агента,
/agent/claim у обоих один и тот же путь. Уборка work/ при старте хаба общая, как сейчас: потерянный запрос агент
повторит. Подпапки на агента не вводим.
8. Проекты: ядро — метка, название, дата; остальное — модули
- Ядро. Таблица
projectостаётся сlabel,name,created_at. Миграция v7 переноситboard_idиboard_typeв таблицу модуля jirajira_project(label PK → project, board_id, board_type)и удаляет колонки изproject(через пересоздание таблицы, как в прежних миграциях). - Число задач
issues_countсчитает модуль jira. - Контракт
Moduleполучает поля проекта:project_fields— описание полей модуля: имя, тип, проверка, можно ли менять после создания, подпись и подсказка для веба и MCP;project_view(conn, labels) -> {label: {поле: значение}}— значения для списка проектов, включая вычисляемые (issues_count,pages_count,synced_at);on_project_created(conn, project, fields, author)— запись своих полей и пакет, как сейчас;on_project_updated(conn, project, fields, author)— правка своих изменяемых полей и пакет.
registry.checkпадает, если имя поля проекта есть у двух модулей или совпадает с полем ядра.GET /projectsотдаёт плоские объекты: поля ядра и поля всех модулей. Набор и значения полей jira прежние:board_id,board_type,issues_count.POST /projects{label, name, author?, <поля модулей>}. Неизвестное поле — 422. Каждый модуль проверяет свои поля до записи; ошибка — 422 с именем поля, ничего не записано.batch_idв ответе прежний: пакет первого модуля, вернувшего пакет. Решение R-8 hub-modules остаётся.- Новый
PATCH /projects/{label}{<изменяемые поля модулей>, author?}. Неизменяемое поле — 422 «<поле> после создания не меняется». Ответ —{project, batch_ids: [...]}. Доска jira остаётся неизменяемой, как сейчас. Корень вики изменяемый: у уже заведённых проектов (AWPCA и др.) его надо проставить потом. - MCP.
core/mcp.register_core_tools(mcp, module)строитprojectsиproject_createсервера модуля из егоproject_fields. Сервер jira отдаёт прежние названия, параметры и описанияproject_createи прежние колонкиprojects(эталоныtests/fixtures/baseline/mcp_tools.expected.jsonне меняются). У сервера wiki — свои колонки и параметрwiki_root. - Веб.
HubModule.projectFields— поля модуля в окне «Новый проект». Поле доски переезжает в модуль jira, текст и подсказка прежние.
Метки долг hub-modules в ядре снимаются все. Тест test_core_has_no_jira_terms больше не делает для них исключения.
9. Модуль wiki: данные
Таблицы модуля (схема v7, рядом с миграцией решений 2, 3, 8):
-
wiki_project:labelPK → project,root_code(DOC-<цифры>),root_title,synced_at(последняя успешная синхронизация),pages_count,sync_error(текст последней неудачи илиNULL). -
wiki_page:- ключ:
label,code; PK(label, code); - поля:
eva_id,title,parent_code,orderno,depth,path(каталог страницы относительноwiki/<label>/),url(адрес страницы в Eva от агента),size(символов Markdown),synced_at.
Страница в двух проектах — две строки. Деревья проектов независимы.
- ключ:
-
wiki_page_fts— FTS5(title, body)сcontentпо Markdown-тексту и токенизаторомunicode61 remove_diacritics 2. Используется только для поиска. Сам текст страниц живёт в файлах. -
wiki_attachment:idINTEGER PK,label,page_code,url(абсолютный адрес в Eva),name,state(none|fetching|ready|failed),file(sha256 вfiles/),size,error,fetched_at. Одна строка на пару «адрес + страница». Адрес, который встречается на нескольких страницах, даёт несколько строк, но файл вfiles/хранится один (sha256). -
wiki_sync_stage— временные пачки страниц текущей синхронизации, ключ(command_id, code). Очищается при завершении команды в любом статусе.
file_refs модуля возвращает file всех wiki_attachment в состоянии ready, поэтому уборка files/ их не
трогает, пока строка жива.
10. Синхронизация wiki_sync
Постановка. Команда wiki_sync {label, root} — тип чтения (read_types), пакет с project = <label>. Ставят её:
- MCP
syncи кнопка «Обновить» в вебе (POST /wiki/sync {project}); - привязка или смена корня (
on_project_created/on_project_updated); - фоновая работа модуля (
Module.tick): по каждому проекту с корнем, если сsynced_at(или с последней неудачи) прошло большеwiki_auto_sync_hours(настройка модуля, по умолчанию 24,0— выключено) и у проекта нет пакетаwiki_syncв работе.
POST /wiki/sync и фоновая работа не ставят пакет, пока предыдущий wiki_sync проекта в работе: ответ 409 с id
пакета, MCP показывает его состояние.
Смена и сброс корня не упираются в это правило. PATCH отменяет ещё не выданные (queued) пакеты wiki_sync
проекта. При новом корне он ставит новый пакет; уже выданный агенту пакет доживает. Отчёт done по команде, у которой
params.root не равен текущему корню проекта (сменён или сброшен), отбрасывается: копия и данные не меняются, команда
done с пометкой в result.discarded = "корень сменился". Типичный случай: корень проставили до установки eva-agent,
потом исправили опечатку.
Агент (контракт; решения внутри агента — его change):
- Берёт корень
CmfDocument.getи все страницы поддерева, какcollect_subtreeвeva_wiki_export.py. - По каждой странице берёт
text. - Разбирает ссылки страницы. Это делает агент, а не хаб: только он знает
eva.base_url. Относительные адреса относятся к хосту Eva (urljoinсeva.base_url).- Вложение —
<img src>/<a href>на хост Eva с путём/files/….- Миниатюра
<адрес>.meta/thumbnail.jpg— это файл по<адрес>; одно вложение, а не два. - Адрес файла и его миниатюры в одной странице — одно вложение.
static/…(значки интерфейса) не вложение: ссылка удаляется из текста.
- Миниатюра
- Ссылка на страницу — ссылка на хост Eva с кодом
DOC-<цифры>в адресе (project/Document/DOC-…). - По каждой ссылке агент шлёт:
ref— значение атрибута ровно как в HTML, по нему хаб находит место замены;url— абсолютный адрес (только у вложения);name—altилиtitleкартинки, текст ссылки, иначе имя файла из пути;code— у ссылки на страницу.
- Вложение —
- Отправляет страницы пачками
POST /agent/wiki/pages {command_id, pages: [{code, eva_id, title, parent_code, orderno, url, html, attachments: [{ref, url, name}], links: [{ref, code}]}]}. В пачке не больше 50 страниц и не больше 4 МБ тела. - Отчитывается
doneсresult = {root_title, codes: [<все коды дерева в порядке обхода>], eva_calls}. - В
wiki_fetchскачивает только адреса на хостеeva.base_url, иначеfailed«чужой хост». Так токен Eva не уходит на чужой адрес.
Хаб.
- Приём пачки. HTML каждой страницы сразу переводится в Markdown, ссылки заменяются по
ref. Вместоidвложения ставится заглушка, её заполняет применение. Результат кладётся вwiki_sync_stage, копия не меняется. Перевод идёт вне транзакции записи (markdownify на десятках МБ занимает секунды) и не держит блокировку базы, в которой ждутclaimjira-agent и веб. - Отчёт
done. Сначала, вне транзакции, Markdown-копия собирается из стадии вwiki/.<label>.new/с предварительно выделеннымиidвложений. Затем одной короткой транзакцией (CommandHooks.on_result):- каталог атомарно подменяется: переименование, старая копия удаляется после подмены;
wiki_pageи FTS пишутся заново;- для
wiki_attachmentпри той же паре «адрес + страница» сохраняютсяidи скачанный файл, новые пары добавляются, пропавшие удаляются; - заполняются
wiki_project.synced_at,pages_countиroot_title,sync_errorобнуляется.
- Код из
codes, для которого не пришла пачка, — командаfailed«нет страницы <код>», копия прежняя. - Отчёт
failedпишетsync_error, копия прежняя. - Повторная выдача чистит
wiki_sync_stageкоманды.
Раскладка копии (по образцу eva_wiki_export.py):
- каталог страницы —
NN_<безопасное название>/внутри каталога родителя;NN— позиция среди соседей, у корня номера нет; - в каталоге
index.mdс frontmattercode,title,url,synced_at, затем# <title>и текст; - в корне
_tree.json— плоский список{code, title, depth, path}; - каталоги 0700, как остальные данные хаба.
Безопасное название — как safe_name скрипта; при совпадении у соседей хаб добавляет _2, _3.
HTML → Markdown делает хаб: зависимость markdownify, ATX-заголовки. Ссылки (классифицирует агент, см. выше):
- картинка или файл Eva заменяется на
[📎 <имя>](attachment:<id>): поidинструментattachmentи веб скачивают файл; - ссылка на страницу Eva с кодом
DOC-<цифры>заменяется на[<текст>](page:<код>): по коду её открывает инструментpage, а в вебе она ведёт на страницу вики, если та есть в дереве проекта; - прочие ссылки остаются как есть.
Объём. Перекачка всего дерева при каждой синхронизации — осознанно (история и частичная перекачка — позже). Темп —
настройка модуля для агента wiki_pacing (пауза между вызовами Eva, по умолчанию 0.2 с).
11. Вложения по запросу wiki_fetch
POST /wiki/attachments/{id}/fetch (веб и MCP) ставит пакет из одной команды wiki_fetch {attachment_id, url}. Это
тип чтения, по образцу modules/jira/fetch.request_fetch: одна транзакция, state = fetching. Пока скачивание идёт,
повторный запрос отвечает текущим состоянием, а не новым пакетом.
Агент:
- скачивает адрес с токеном Eva;
- больше
MAX_SIZEядра (10 МБ, поContent-Lengthили по факту чтения) —failed«файл больше 10 МБ — откройте в Eva»; - иначе
POST /agent/filesи отчётdone {file, size}.
Хаб проверяет файл в files/ и ставит state = ready. Если файла нет или размер чужой — failed.
Выход:
GET /wiki/attachments/{id}— состояние;GET /wiki/attachments/{id}/content— сам файл;- MCP
attachmentкладёт копию вdownloads/<id>/<имя>(общая папка ядра, уборка 30 дней). Имена каталогов jira и wiki не пересекаются: каталог wiki —downloads/wiki-<id>/.
12. MCP вики
Запуск. jira-hub mcp wiki [--project <метка>], имя сервера jira-hub-wiki (переименование в sdlc-hub — позже,
вместе со всем). Регистрация в awp-ca — новая запись jira-hub-wiki в .mcp.json с --project AWPCA; её делает
владелец. Хаб выключен — та же подсказка, что у jira.
Инструменты (короткие имена: в Claude они видны как mcp__jira-hub-wiki__<имя>):
| Инструмент | Что делает |
|---|---|
projects |
проекты: метка, название, корень, страниц, синхронизация |
project_create |
label, name, wiki_root (необязательно) |
root_set |
root — корень текущего проекта (PATCH /projects/{label}) |
tree |
code (необязательно, по умолчанию корень), depth (по умолчанию 2) — дерево с кодами и размерами |
page |
code или title (точное совпадение, иначе до 10 кандидатов), offset — Markdown страницы с шапкой (код, путь в дереве, адрес Eva, дата синхронизации), длинный текст — частями по 20 000 символов |
search |
query, limit (по умолчанию 10) — страницы с фрагментами, FTS5; слова запроса ищутся по префиксу |
attachment |
id — скачать вложение (как attachment у jira: ждёт до 60 с, затем путь к копии) |
sync |
синхронизировать сейчас, wait (по умолчанию нет) |
status |
корень, последняя синхронизация, ошибка, пакет в работе, агент wiki (онлайн, версия, публикация, обновление — как agent_status jira) |
Инструменты, кроме projects и project_create, требуют привязки к проекту, как у jira. Без корня они отвечают
«у проекта <метка> нет корня вики: root_set <DOC-…>». Корень есть, а синхронизаций ещё не было — «вики проекта ещё
не синхронизирована: sync».
13. Веб
Модуль web/src/modules/wiki/:
- страница «Вики»
/app/wiki?project=<метка>, пункт боковой панели; - дерево слева: свёрнутое, раскрывается по клику;
- страница справа: Markdown, без сырого HTML;
- поиск над деревом;
- кнопка «Обновить» и плашка хода синхронизации в боковой панели — как у jira;
- ссылки:
page:ведёт на страницу вики,attachment:скачивает файл (запрос, опрос,api.blob, как вложения в карточке задачи); - без корня — форма «Корень вики (DOC-…)» (
PATCH /projects); - окно «Новый проект» получает поле «Корень вики (необязательно)» через
projectFields.
Маршруты HTTP модуля (клиентский токен):
GET /wiki/tree?project=— дерево с кодами;GET /wiki/pages/{code}?project=—{code, title, path, url, synced_at, markdown, attachments};GET /wiki/search?project=&q=&limit=;POST /wiki/sync;GET /wiki/status?project=;- вложения из решения 11.
Отрисовка Markdown — react-markdown с remark-gfm (таблицы), без rehype-raw.
14. eva-agent
Репозиторий aep/eva-agent (создан владельцем пустым 24.10.2026), каталог jira-bridge/eva-agent/. Обвязка
подключается adopt-project.sh. Решения агента — его design-only change. Хаб фиксирует только контракт:
- Стек: Python 3.9 на stdlib, одним файлом
src/agent.pyсо строкойAGENT_VERSION = <целое>. Запускательsrc/run.py— копия запускателя jira-agent. Именаagent.py,update.jsonи протокол самообновления прежние, иначеrun.pyне подходит. - Конфиг
agent.json:hub_url(folder:<путь>или http),hub_token— токен агентаwikiизjira-hub tokens,eva: {base_url, token},verify_ssl,ca_bundle,timeout,poll_seconds,auto_update. Токен Eva — только здесь. - Протокол с хабом — как у jira-agent:
claim,GET /agent/settings,GET /agent/commands/{id}, результат,GET /agent/release,POST /agent/files; плюсPOST /agent/wiki/pages. - Команды:
wiki_sync,wiki_fetch(решения 10, 11). Обе только читают Eva: журнал на диске нужен для повторного отчёта, но «проверь, потом сделай» не нужно. - Из jira-agent копируются, без общей библиотеки: HTTP-слой,
FolderTransport,HubClient(без jira-маршрутов),Journal,Runner,Updater,main. Решение владельца 23.10.2026: однофайловый агент на виртуалке. - Запуск на той же виртуалке, отдельным каталогом и процессом со своей блокировкой. Первую установку делает
владелец руками, дальше агент обновляется публикацией
jira-hub agent publish --module wiki.
15. Миграция v7 одним шагом
Один шаг MIGRATIONS 6 → 7 с копией базы перед ним (механизм прежний):
tokens.agent→tokens.agents.jira, токенagents.wikiсоздаётся.agent_statusпересоздаётся по агентам (решение 3).projectтеряетboard_id/board_type, появляетсяjira_projectс перенесёнными значениями.- Создаются таблицы wiki (решение 9).
Перенос release/ в release/jira/ — не миграция базы, а шаг старта хаба (решение 5).
16. Порядок работ
- Ядро: виды агентов (решения 1–7) — jira-agent v14 работает. Живой проверки посередине нет: launchd запускает хаб
из основного checkout, а частичная v7 на живой базе сломала бы последующие шаги миграции. Совместимость с v14
проверяется тестом: снимок схемы v6 с досками и токеном → миграция →
claim,GET /agent/settings,/agent/stateс прежним токеном, формой и ответами v14. Живая проверка — после merge. - Ядро: поля проектов (решение 8) и снятие долга.
- Модуль wiki в хабе: данные, синхронизация с имитацией агента в тестах, вложения, HTTP, MCP.
- Веб.
- eva-agent в своём репозитории: отдельный PR и отдельный план, после merge хаба.
Шаги 1–4 — этот change и один PR в jira-hub. Шаг 5 — PR в eva-agent и его design-only change.
Risks / Trade-offs
- Миграция v7 трогает живые данные: токены, статус агента, доски. Защищают копия базы перед миграцией и тест
миграции на снимке v6 (
tests/fixtures/из настоящей схемы v6 с досками AWPCA = 4216 scrum). Если jira-agent после выкатки получает 401, это ошибка переноса токена; откат — копия базы. - jira-agent v14 видит другой
/agent/settings(решение 6). Это проверяется тестом на равенство ответа эталону v6, кромеboard_id. - FTS5 в Python от uv. Если модуль
fts5в SQLite не собран, поиск сломается. Тест на старте модуля проверяетCREATE VIRTUAL TABLE … USING fts5; если не вышло — поиск идёт черезLIKEпо файлам и в логе одно предупреждение. - Русская морфология. FTS5 ищет по префиксу слова, «статусы» не найдёт по «статус…» в другой форме корня. Принимаем; MCP подсказывает искать по корню слова.
- Большое дерево. Сотни страниц по несколько сотен КБ HTML — пачки по 50 страниц и 4 МБ. Стадия хранится в базе; копия базы перед командой не делается (чтение).
- Атомарность копии. Подмена каталога переименованием. Если хаб упал между удалением старой и переименованием
новой, при старте модуль доводит подмену по наличию
wiki/.<label>.new/. - Агент
wikiесть в хабе до установки eva-agent.statusиGET /agentsпоказывают его офлайн с подсказкой «eva-agent не выходил на связь». Пакетыwiki_syncстоят в очереди и не мешают jira (решение 4). Фоновая синхронизация не ставит новый пакет, пока предыдущий не выдан.
Migration Plan
- Хаб: merge →
git pull→ сразу kickstart (владелец) →make web→/mcpreconnect. jira-agent не обновляется. До kickstart не запускатьjira-hub mcpи CLI (tokens,agent publish): новый код при подключении к базе мигрирует её в v7, а хаб на старом коде после этого не работает (нет колонкиagent_status.id, токенаtokens.agent). Старый фронт работает с новым API, поэтомуmake webможно после kickstart. - После kickstart:
- проверить
agent_statusjira (v14 онлайн); - проставить корни проектам (
root_setили веб); - добавить
jira-hub-wikiв.mcp.jsonawp-ca.
- проверить
- eva-agent: отдельный PR и ручная установка на виртуалке владельцем; дальше публикация
--module wiki. - Откат хаба: предыдущий коммит, копия базы v6 из
backups/и возвратrelease/jira/agent.py,manifest.jsonвrelease/(иначе старый код увидит «публикации нет»; для v14 безвредно, самообновление молчит).
Decisions made without discussion
Принятые агентом в brainstorming 24.10.2026; владелец видит их при ревью документа.
- Вид агента = имя модуля (
wiki, а неeva). Обоснование: см. решение 1. Цена ошибки — переименование ключа вtokens.agentsиagent_status. - Агенты работают параллельно, «одна в полёте» — на агента (решение 4). Обоснование: синхронизация вики не должна
держать Jira. Цена ошибки — правка одного условия в
claim. - Реле общее, без подпапок (решение 7). Обоснование: реле агентов не различает и не должно. Цена ошибки — подпапки и вторая настройка, если папка станет узким местом.
- Корень вики меняется после создания, доска — нет (решение 8). Обоснование: у живых проектов корня нет. Цена ошибки — запрет в описании поля.
- Перекачка всего дерева каждый раз (решение 10). Обоснование: дата изменения в Eva неизвестна, история отложена.
Цена ошибки — нагрузка на Eva при большом дереве; лечится
wiki_pacingи частичной перекачкой позже. - Текст страниц в файлах, в базе — индекс FTS (решение 9). Обоснование: копия — главный артефакт, база — для поиска. Цена ошибки — двойной источник; синхронизация пишет оба в одной транзакции и подмене.
- Ссылки
attachment:<id>иpage:<код>в Markdown (решение 10). Обоснование: по ним работают инструменты и веб. Цена ошибки — формат ссылок в файлах; меняется со следующей синхронизацией. - Предел вложения 10 МБ — общий
MAX_SIZEядра (решение 11). Обоснование: реле передаёт файл base64 в JSON. Цена ошибки — большие файлы Eva только по ссылке в Eva. - Имя MCP-сервера
jira-hub-wiki, инструменты без префикса (решение 12). Цена ошибки — правка.mcp.jsonв awp-ca. - Отрисовка Markdown —
react-markdown+remark-gfm(решение 13). Цена ошибки — замена библиотеки на странице вики. - Ежедневная синхронизация — фоновой работой хаба (решение 10). Обоснование: прецедент — автообновление
зеркала jira. Цена ошибки — отключение настройкой
wiki_auto_sync_hours = 0. - Одна живая проверка — после merge, без промежуточной (решение 16; builder check). Обоснование: частичная v7 на живой базе ломает последующие шаги, а launchd смотрит в основной checkout. Цена ошибки — несовместимость с v14 всплывёт после выкатки; страхуют тест миграции на снимке v6 и откат на копию базы.
- HTML → Markdown при приёме пачки, вне транзакции (решение 10; builder check). Обоснование: перевод десятков МБ
под
BEGIN IMMEDIATEблокируетclaimjira-agent и веб. Цена ошибки — стадия в базе крупнее (Markdown вместо HTML — обычно меньше). - Ссылки классифицирует агент, хаб заменяет по
ref(решение 10; builder check). Обоснование: адрес Eva знает только агент, хабу он не нужен. Цена ошибки — контрактpagesменяется в обоих репозиториях. wiki_fetchтолько на хостeva.base_url(решение 10; builder check). Обоснование: токен Eva не уходит на чужой адрес. Цена ошибки — вложение на другом хосте открывается только ссылкой.- Смена корня отменяет невыданные
wiki_sync, позднийdoneстарого корня отбрасывается (решение 10; builder check). Обоснование: корень правят, пока eva-agent ещё не установлен; копия старого дерева не должна встать под новый корень. Цена ошибки — лишняя синхронизация. - Тестовые HTML-страницы — синтетические, по реальным форматам адресов из
awp-ca/docs/source/tz/(Context). Сам текст ТЗ в фикстуры не копируется. Обоснование: сырого HTML Eva нет, а содержимое ТЗ банка не место в репозитории хаба. Цена ошибки — разметку вокруг ссылок (особенно у прикреплённых файлов) уточнять по первой живой синхронизации; агент и хаб правятся вместе.
Решения контроллера при исполнении плана (2026-10-24, SDD).
- R-1. 2026-10-24. Строки
agent_statusдляregistry.agent_modules()вставляютсяINSERT OR IGNOREпри каждомdb.migrate, а не только вMIGRATIONS[6]. Обоснование: wiki-агент появляется после схемы 7 (Task 5), иначе у него нет строки статуса. Цена ошибки: падениеclaimwiki; правка одного места. - R-2. 2026-10-24. Ответ
POST /projects— по спекеhub-api:{project, batch_id},batch_id— пакет первого модуля, вернувшего пакет;batch_idsв ответе POST не добавляем (исполнитель Task 3 добавил, ревью отклонило как лишнее). Цена ошибки: клиент не видит второй пакет безGET /batches. - R-3. 2026-10-24.
test_core_has_no_module_termsищетevaпо границе слова (\beva\b,eva-agent), аwiki_,wiki/,DOC-— подстрокой. Обоснование: подстрокаevaдаёт ложные срабатывания (evaluate,level). Цена ошибки: правка регулярки. - R-4. 2026-10-24. В Task 5
on_project_updated(None)чиститwiki_page_ftsтолько если таблица существует (создаёт её Task 7). Цена ошибки: FTS-строки сброшенного корня живут до Task 7.
Решения исполнителя Task 1 (2026-10-24):
- Фикстура
tests/fixtures/settings_v6.json— замороженные умолчания настроек v6 дляmake_v6_db, чтобы v6-база не зависела от текущего кода. Цена ошибки: лишний файл фикстуры. queue.claim/agent_version/agent_viewполучили необязательныйagent(умолчание — первый агент реестра, в ядре нет слова jira); Task 2 передаёт агента явно. Цена ошибки: до Task 2 поведение равно jira.- Служебный ключ
_agentперезаписывается диспетчером для маршрутов scopeagent, поэтому адрес запроса его не подделает. Цена ошибки: нет. current_command_idосвобождаетсяWHERE current_command_id = ?без имени агента (id команды уникален). Цена ошибки: нет.h_claimпринимаетlast_call_atкак алиасlast_jira_call_at(решение 3). Цена ошибки: нет._authдля агента сравнивает токен со всеми агентами без раннего выхода (постоянное время). Цена ошибки: нет._tokens_v7: нетtokensили уже естьagents— шаг пропускается (идемпотентность). Цена ошибки: нет.
Решения исполнителя Task 2 (2026-10-24):
module_dirсоздаёт каталог модуля (0700) только если его нет; права существующих каталогов выставляетHubпри старте (иagent publishдля корня), а неclaim. Цена ошибки: чужие права на каталогах агентов перезаписываются при старте.Hub(release_dir=)переименован вrelease_root=. Цена ошибки: нет (вызовов с kwarg не было).GET /agent?module=отвечает 404not_foundи для модуля без команд. Цена ошибки: клиент не отличает «нет модуля» от «нет агента».Hub.agent_updateстроится поregistry.agent_modules(); для прочих имён — пустой словарь (_no_update).queue.claim: параметрlast_jira_call_atпереименован вlast_call_at; тело HTTP принимает оба ключа.migrate_layoutпереносит файлы покадрово (agent.py, затем manifest.json), повторяется, пока нетjira/manifest.json; существующий приёмник не перезаписывается, корневой файл удаляется. Цена ошибки: если старый код успел записать в корень более новый agent.py после частичной миграции, он потеряется (риск низкий).registry.DEFAULT_AGENT = "jira"— единственное место с умолчанием агента в ядре;release.pyимпортирует его оттуда. Цена ошибки: нет.
Решения исполнителя Task 3 (2026-10-24):
- Миграция v7 убирает колонки досок
ALTER TABLE project DROP COLUMN, а не пересозданием таблицы: приforeign_keys=ONDROP TABLE projectломается из-за дочернейjira_project. Нужен SQLite не ниже 3.35 (в окружении 3.51). Цена ошибки: на старом SQLite миграция падает; копия базы сделана до неё. jira_projects.create(conn, label, name, board_id)— точка создания проекта с доской для init, миграции v3 и тестов; доска проверяется раньше метки и названия. Цена ошибки: другое сообщение при одновременно плохих метке и доске.POST /projects: проверка ядра (label,name), затем полей модулей (неизвестное — 422<поле>: неизвестное поле), затем 409 на дубль; модулям без своих полей вon_project_createdпередаётся пустой dict.PATCH /projects/{label}: проект проверяется (404) раньше полей; пустое тело — 200 сbatch_ids: [];authorнеобязателен (по умолчаниюapi).HubClient.patchдобавлен в ядро (нужен тестам иroot_set). Метка в адресе PATCH раскодируетсяunquote.register_core_toolsсоздаёт инструментprojectsвнутри; функцийprojects/project_createвcore.mcpбольше нет, тесты берут их изmcp._tool_managerиjmcp.project_create.- Проверка «изменяемое поле модуля без
on_project_updated» выполняется при старте вregistry.check, а не 422 при запросе (исполнитель Task 3, fix). Цена ошибки: модуль с таким полем не запустится; правится в модуле. - Ключ
authorвPOST/PATCH /projects— служебный и полем проекта не считается. Цена ошибки: модуль не может назвать своё полеauthor(запрещеноregistry.check).
Решения исполнителя Task 4 (2026-10-24):
- Правка
web/e2e/hub.tsиfeed.spec.ts(tokens().agents.jira) сделана в Task 4: после Task 1 формат токенов изменился, e2e без правки не зелёные. Цена ошибки: нет (только тесты). Project—{label, name, created_at} & Record<string, unknown>; типы jira (JiraProject) вmodules/jira/projectFields.ts.useUpdateProject({label, fields})сам добавляетauthor: "viewer"; сигнатура может уточниться в Task 10. Цена ошибки: мелкая правка.- Поля окна — единый
TextInput(number/text), вёрстка не менялась;min=1только для int.
Решения исполнителя Task 5 и рулинг R-5 (2026-10-24):
- R-5. Имя каталога копии проекта —
paths.dir_name(label):%,/,\экранируются (%25,%2F,%5C), ведущая.—%2E(.→%2E,..→%2E.; уточнено финальной волной, M-14). Обычные метки не меняются. Обоснование: метки ядра допускают/и.., без экранирования сборка иrmtreeвыходили бы за каталог вики. Раскладка копии (Task 7) идёт только черезpaths.project_dir. Цена ошибки: экзотические метки получают экранированное имя каталога. PATCH wiki_rootс тем же кодом — no-op (batch_ids: []): повторная синхронизация идёт черезPOST /wiki/sync. Цена ошибки: одна строка вon_updated.on_updatedотменяет только пакетыwiki_sync, все команды которыхqueued; выданный (claimed) не трогает — его позднийdoneпо старому корню отбросит Task 7. Цена ошибки: лишняя синхронизация.- Автосинхронизация проекта, у которого нет ни
synced_at, ниfailed_atи нет активного пакета, ставится сразу, не ожидая часов. Цена ошибки: лишний пакет; зависит от записиfailed_at/synced_atв Task 7. Прочие решения исполнителя (3, 5–9, 11–12 отчёта):paths.rootна базе без файла бросаетRuntimeError,_drop_copyего ловит; очистка FTS по rowid страницы; строгая валидация настроек и команд (bool не целое);wiki_project.failed_atпока не пишется. Цена ошибки: правка одной строки в соответствующем месте.
Решения исполнителя Task 6 (2026-10-24; HTML Eva недоверенный):
- Вложения дедуплицируются по
url: миниатюра и файл — одно вложение, если агент шлёт одинаковыйurl. Цена ошибки: при разныхurlв eva-agent появятся две ссылки; проверяется по первой живой синхронизации. - Картинка внутри
<a>на вложение выдаёт только ссылку<a>. Цена ошибки: при разныхrefпоказана одна ссылка. - Небезопасные ссылки (
javascript:,data:,vbscript:после удаления управляющих символов и пробелов) дают только текст, у картинок пусто; ссылки и картинки автора со схемойattachment:— текст/пусто. Цена ошибки: видимый текст без ссылки. - Ссылку вложения порождает только конвертер: приватный маркер U+E000/U+E001, авторские U+E000/U+E001 из HTML
удаляются; любое авторское
attachment:@нейтрализуется вставкой U+200C между:и@;fill_idsзаменяет только](attachment:@N), индекс вне списка —ValueError(вызывающий Task 7 превращает вfailed). Цена ошибки: невидимый символ в сыром.mdпри авторском «attachment:@»; лишние PUA-символы удаляются. - Текст
<вне кода выводится<(сырых тегов нет); внутри<code>/<pre>не меняется. Имя вложения экранирует\ [ ] <и_/*на границе слова.@не экранируется. Цена ошибки: сырой.mdс<. - Известные края (отложены): вложение в
<a>страницы/внешней ссылки, вложенный<span>между<a>и<img>, хвостовой\в тексте ссылки, пробелы/скобки во внешнем href, дубликатыref(побеждает последний).
Решения исполнителя Task 7 и рулинг R-6 (2026-10-24):
-
R-6. В контракт
CommandHooksядра добавлены два необязательных хука:before_result(вызывается до транзакции отчёта; нужен, чтобы перевод и запись файлов шли внеBEGIN IMMEDIATE, решение 10) иabort_result(queue.report_resultвызывает его вfinally, если отчёт не дошёл доafter_result). У jira обаNone. Цена ошибки: правка контракта модуля; сборку можно вернуть вon_resultценой блокировки очереди на время сборки. -
Идентификаторы вложений выделяются короткой транзакцией до сборки копии; при отбрасывании или сбое добавленные строки
state='none'удаляются (abort_result). Подготовка хранится в памяти процесса; после рестарта хаба повтор отчёта готовит заново. Цена ошибки: сироты до следующей синхронизации. -
Подготовка идёт в
.<имя>.new.<id команды>под per-labelLockотbefore_resultдоafter_result/abort_result; второй одновременныйdoneждёт первого (без таймаута) и затем выходит идемпотентно.finish_pending_swapsпродвигает.new*только при наличии_tree.json. Цена ошибки: долгая сборка проекта задерживает отчёты по нему. -
codes[0]обязан быть корнем команды; страницы без родителя и циклы цепляются к корню;NNв имени каталога — позиция среди соседей (у корня каталог по названию, без номера); дубликаты имён —_2,_3без учёта регистра; имя до 80 символов и 200 байт. Цена ошибки: другие имена каталогов. -
FTS-таблица
wiki_page_ftsсоздаётся лениво вsearch.ensure(не вschema.SQL), эталонschemaне меняется; токены запроса —\w+. Сбой подмены каталога откатывает строки и переводит команду вfailed. Падение внешнего COMMIT после подмены оставляет новые файлы при старых строках до следующей синхронизации. Цена ошибки: лишняя синхронизация. -
failed_atпишется приfailed, отброшенной и отменённой синхронизации (иначе автотик ставит пакеты каждый тик);housekeeping_tickудаляет стадию команд не вqueued/claimed. Лимитыstage:code200,title1000,url4000, до 20000 страниц на команду; форматDOC-\d+дляcodeне навязывается. Пустыеparent_code/url—None.ValueErrorизbefore_result→ 500;sqlite3.OperationalErrorиз подготовки → 500 (повтор агентом). -
R-7.
GET /wiki/searchотвечает{"results": [...]}, а не голым списком: все ответы хаба — объекты ({"pages": …}уtree), план и клиенты (MCP, веб) строятся на этом. Спекаwikiисправлена. Цена ошибки: правка формата ответа и клиентов.
Решения исполнителя Task 8 (2026-10-24):
limitвне 1–50 или не число — 422bad_limit(не обрезка); нетproject— 422bad_request; вsync_in_progressdetail = {"batch_id": id}. Цена ошибки: строже, чем нужно.markdownстраницы —index.mdбез front matter, заголовок# …остаётся; порядокtree— по_tree.jsonкопии (запасной — по частям пути). Цена ошибки: клиент срезает дубль заголовка сам.- Зависшее
fetchingбез живой командыwiki_fetch(пакет отменён): повторный fetch ставит новый пакет вместо ответа «как есть», иначе вложение застряло бы навсегда. Цена ошибки: лишний пакет. - Скачивание вложения:
Content-Type: application/octet-stream+nosniff, имя вfilename*=UTF-8''черезquote(safe=""), пустое имя —attachment-<id>; fetch отвечает 202 при новой постановке, 200 иначе; код страницы в адресе раскодируетсяunquote(%2F). Цена ошибки: клиент должен брать basename имени сам.
Решения исполнителя Task 9 (2026-10-24):
- Отказы MCP «нет корня»/«не синхронизирована» берутся из
GET /projects(поляwiki_root,wiki_synced_at), а не из 409 хаба: спека требует, чтобы запроса/wiki/treeне было. Цена ошибки: лишний запрос на вызов. root_setвалидируется локально той жеparse_root; пустое значение — отказ (сброс корня через MCP не предусмотрен, толькоPATCH {wiki_root: null}). Имя копии вложения: basename →safe_namejira → каталогdownloads/wiki-<id>0700 через общийstore_copy.fmt_agent,safe_name,store_copyперенесены вcore/mcp.pyбез смены текстов (jira реэкспортирует под прежними именами). Цена ошибки: нет.- Формат вывода
page/tree/search/status(спека задаёт состав, не текст):tree— «(N симв.)»,depth— уровни ниже стартовой страницы (по умолчанию 2);pageпо названию — точное совпадение открывает страницу, иначе до 10 кандидатов и «…и ещё N»;offsetза концом — «offset=N за концом текста: длина L».sync(wait)опрашивает пакет раз в 2 с до 300 с; при идущей синхронизации — «уже идёт: пакет #N». Цена ошибки: правка текстов.
Решения исполнителя Task 10 (2026-10-24):
- Дерево: корень раскрыт (виден depth 1), глубже свёрнуто; узлы —
role=treeitemсaria-level, шеврон только переключает; предки открытой страницы раскрываются при её смене и при появлении узла вnodes, но не при перечитывании дерева (свёрнутый пользователем родитель остаётся свёрнутым). Цена ошибки: другая раскрытость. - Плашка синхронизации в боковой панели получает
role="status"только при ходе пакета или ошибке (иначе ломался e2eboard.spec);GET /wiki/statusв панели запрашивается только у проекта сwiki_root. Цена ошибки: покойное состояние не озвучивается. - Ведущий
# название, совпадающий с заголовком страницы, скрывается; внешние Markdown-картинки не загружаются (показывается «[изображение: alt]»); «открыть в Eva» — только для http(s). Цена ошибки: нет inline-картинок. urlTransformпропускает толькоpage:,attachment:,http(s):и относительные адреса; режетjavascript:,data:,vbscript:(после удаления управляющих символов и пробелов, без учёта регистра) и protocol-relative//host,/\host. Опрос вложения ограничен 60 попытками (≈2 мин) с текстом ошибки. Функция сохранения файла скопирована вwiki/download.ts(модули не зависят друг от друга);fmtStampпока импортируется из jira (долг). Цена ошибки: дубль ~10 строк.- Сид e2e: копия вики AWPCA (корень DOC-9001, 2 страницы, одно вложение) собирается
sync.build_copyвJIRA_HUB_HOMEтестового хаба. Ответ 409 на «Обновить вики» ошибкой не показывается — статус перечитывается.
Финальное ревью и волна правок (2026-10-24):
- R-8. Финальное ревью (opus) нашло три Important и 15 Minor; одна волна правок закрыла I-1, I-3 и M-1…M-6,
M-8…M-15; I-2 и M-13 — документация (порядок выкатки и откат, Migration Plan выше). Не сделаны: M-7 (замер FTS под
BEGIN IMMEDIATEна живой синхронизации) и T8.3 (отказcheck_batchпри активнойwiki_sync) — follow-up. - Агент модуля не читает и не закрывает команды чужого модуля: тип вне
queue._own_types(агент)→ 404not_found, неотличимо от несуществующей команды (I-1). Цена ошибки: eva-agent с устаревшим журналом не затронет команды jira. - В ядро добавлено необязательное
ProjectField.validate_edit— более строгая проверка приPATCH(wiki_root: ""→ 422, сброс корня толькоnull; POST с""допустим) (M-6). Цена ошибки: нет, у остальных полей поведение прежнее. - Одиночный суррогат в строках страниц заменяется на «?» кодеком (M-8);
parse_rootвынесен вwiki/roots.py, MCP вики не тянетdb/queue(M-5); общийdb.has_table;release._warned— словарь путь→текст;agent publishвызываетmigrate_layout; зависимостиbeautifulsoup4>=4.12,markdownify>=1.2.3,<2; вpyproject.tomlper-file-ignores F811 дляtests/test_wiki_api.py; ruff вmake checkне добавлен (решение владельца).