docs: ревизия спека плагина по ревью Codex (канал dist, контракт лаунчера, редакция навыков)

This commit is contained in:
2026-07-31 00:52:52 +03:00
parent 63bd9db5f3
commit 7cc4ee7447
@@ -1,6 +1,6 @@
# Спек: упаковка kompas3d-mcp в плагин Claude Code и публикация через Gitea
Дата: 2026-07-31 · Статус: согласовано, к реализации
Дата: 2026-07-31 · Статус: согласовано, ревизия 2 (после ревью Codex)
## 1. Цель
@@ -22,14 +22,25 @@ README сразу рассчитаны на постороннего, но вы
`obsidian-autodoc`).
- `home-repo-cc`**приватный** (Gitea API отдаёт 404 без токена). `kompas3d-mcp`**публичный**,
релизы включены.
- Marketplace поддерживает source-тип **`git-subdir`** (`{url, path, ref?, sha?}`, разрежённый клон)
плагин может физически жить в подкаталоге другого репозитория.
- Плагин объявляет MCP-серверы через `.mcp.json` в своём корне; в путях доступна переменная
`${CLAUDE_PLUGIN_ROOT}`. Плагин копируется в кеш `~/.claude/plugins/cache` при установке.
- Marketplace поддерживает source-тип **`git-subdir`** (`{url, path, ref?, sha?}`, разрежённый клон).
- Плагин объявляет MCP-серверы через `.mcp.json` в своём корне; в путях доступна
`${CLAUDE_PLUGIN_ROOT}`. Плагин копируется в кеш `~/.claude/plugins/cache`; **версия из
`plugin.json` служит ключом кеша** — при совпадении версии `/plugin update` и автообновление
плагин пропускают.
- `/plugin marketplace update` обновляет **каталог**, а не установленный плагин; плагин обновляется
`/plugin update`.
- **Фоновое автообновление каталога отключает git-credential-helper**, поэтому для приватного
HTTPS-каталога фоновый `git pull` не аутентифицируется и Claude Code уходит в ре-клон (который
может отваливаться по 120-секундному таймауту). SSH-ремоуты этим не затронуты.
- `.agents/skills/` в этом репозитории — побайтово идентичная копия `.claude/skills/` (untracked):
дублирование навыков между харнессами уже началось вручную.
- Навык `kompas-3d` **устарел и не самодостаточен**: заявляет 46 инструментов (в CLAUDE.md — 83),
ссылается на `usecases/`, `src/`, субагентов `kompas-sdk-research` и `docs-maintainer`, которых в
плагине не будет.
- Сервер: `net8.0-windows`, x64, вендорские interop-DLL АСКОН в `libs/kompas-interop` (4.4 МБ,
в репозитории). Ни WinForms, ни WPF не используются.
в репозитории, `Private=true` → копируются в вывод publish). Ни WinForms, ни WPF не используются.
- Отдельного `LICENSE` в репозитории нет; README говорит лишь «распространяются согласно условиям
АСКОН».
## 3. Принятые решения
@@ -41,6 +52,8 @@ README сразу рассчитаны на постороннего, но вы
| Каталог | остаётся приватным `home-repo-cc`, доступ знакомым выдаётся в Gitea |
| Раскладка | плагин лежит в `kompas3d-mcp/plugin/`, каталог ссылается через `git-subdir` |
| Имя плагина | `kompas` |
| Канал дистрибуции | ветка **`dist`**, а не `main` (см. §6) |
| Навыки | публикуется **пользовательская редакция**; материал по разработке сервера выносится (см. §5) |
`kompas`, а не `kompas-3d`: пространство имён даёт `/kompas:doctor`, навыки становятся
`kompas:kompas-3d` / `kompas:kompas-fdm-design`, и это совпадает с именем MCP-сервера (`mcp__kompas__*`).
@@ -54,7 +67,7 @@ README сразу рассчитаны на постороннего, но вы
kompas3d-mcp/
plugin/ ← весь плагин, единственный источник истины навыков
.claude-plugin/plugin.json
skills/kompas-3d/SKILL.md
skills/kompas-3d/SKILL.md ← пользовательская редакция
skills/kompas-fdm-design/{SKILL.md,references/}
commands/doctor.md → /kompas:doctor
.mcp.json
@@ -62,6 +75,7 @@ kompas3d-mcp/
server.lock.json
README.md
adapters/{codex,opencode}/
.claude/skills/kompas-mcp-dev/ ← материал по разработке сервера (не публикуется)
tools/sync-agent-assets.ps1
.gitea/workflows/{ci.yml,release.yml}
```
@@ -69,82 +83,152 @@ kompas3d-mcp/
`plugin/.claude-plugin/plugin.json`: `name: "kompas"`, `displayName`, `description`, `version`
(семвер, поднимается релизным коммитом), `author`, `homepage`, `repository`, `license`, `keywords`.
## 5. Источник истины навыков
## 5. Навыки: пользовательская редакция и источник истины
Навыки хранятся **только** в `plugin/skills/`. `tools/sync-agent-assets.ps1` создаёт на них junction'ы
из `.claude/skills/<name>` и `.agents/skills/<name>` (на Windows junction создаётся без прав
администратора); если junction создать не удалось — копирует и печатает предупреждение. Обе целевые
папки добавляются в `.gitignore`, прежние копии удаляются из индекса.
Публиковать нынешний `kompas-3d` нельзя: он написан для работы **внутри этого репозитория**.
Перед первым релизом навык разделяется:
Смысл: сегодняшний дрейф между `.claude` и `.agents` устраняется структурно. Для локальной работы в
этом репозитории плагин **не устанавливается** — иначе навыки задвоятся (плагинная копия + junction).
- `plugin/skills/kompas-3d/SKILL.md`**пользовательская редакция**: только методика построения
через MCP-инструменты. Убираются ссылки на `usecases/`, `src/`, субагентов и предписание
«заведи кейс и продуктизируй инструмент»; список инструментов приводится в соответствие с
фактическим (сверять по `README.md` §«Инструменты», не по памяти).
- `.claude/skills/kompas-mcp-dev/SKILL.md` — всё вынутое (полигон `usecases/`, продуктизация
инструментов, субагенты). Это **перевалочный пункт**: доводка принадлежит отложенной работе по
рабочему циклу репозитория, здесь только сохраняем материал, не теряя его.
## 6. Лаунчер и версионирование сервера
`kompas-fdm-design` переносится как есть (он опирается на `kompas-3d` и `export_step`, внешних
ссылок на репозиторий не содержит) — проверить это отдельным чтением перед переносом.
`plugin/server.lock.json` — пин ровно одной версии:
Навыки хранятся **только** в `plugin/skills/`. `tools/sync-agent-assets.ps1` создаёт junction'ы
`.claude/skills/<name>` и `.agents/skills/<name>``plugin/skills/<name>`:
- junction на Windows создаётся без прав администратора и может указывать на другой локальный том;
- **если junction создать не удалось — скрипт завершается ошибкой**, а не молча копирует: иначе
«единственный источник истины» превращается в две расходящиеся копии. Копия допустима только
явным флагом `-AllowCopy`, и тогда `sync -Check` обязан сверять хеши;
- `.gitignore` пополняется **точечными путями** (`/.claude/skills/kompas-3d/`,
`/.claude/skills/kompas-fdm-design/`, те же в `.agents/`), а не каталогами целиком: git видит
junction как обычный каталог и без ignore проиндексирует содержимое повторно;
- скрипт никогда не удаляет рекурсивно то, что не является созданным им junction'ом; удаление
делается без следования в target;
- при чекауте внутри OneDrive-дерева поведение не гарантируется — README рекомендует держать
репозиторий вне синхронизируемой папки.
Для локальной работы в этом репозитории плагин **не устанавливается** — иначе навыки задвоятся.
## 6. Канал дистрибуции: ветка `dist`
Каталог **не** может ссылаться на `main`: релиз собирается из отмеченного тегом коммита A, а
бот-коммит с обновлённым `plugin.json` попадёт в текущую голову `main` (уже коммит B). Пользователь
получил бы содержимое `plugin/` из B под версией, чей бинарь собран из A.
Решение: релизный workflow формирует ветку **`dist`** — коммит **строго поверх отмеченного тегом
коммита**, добавляющий только `server.lock.json` и `version` в `plugin.json`. Каталог ссылается на
`ref: "dist"`. `main` остаётся веткой разработки, релизных коммитов в неё нет.
`server.lock.json` фиксирует и происхождение сборки:
```json
{ "version": "1.0.0",
"sourceSha": "63bd9db…",
"url": "https://git.shahovalov.ru/mikhail/kompas3d-mcp/releases/download/v1.0.0/kompas-mcp-1.0.0-win-x64.zip",
"sha256": "…" }
```
## 7. Лаунчер
`plugin/.mcp.json`:
```json
{ "mcpServers": { "kompas": {
"command": "powershell",
"args": ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File",
"args": ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File",
"${CLAUDE_PLUGIN_ROOT}/scripts/launch-kompas-mcp.ps1"] } } }
```
Алгоритм `launch-kompas-mcp.ps1`:
1. Задан `KOMPAS_MCP_EXE` → использовать его (цикл разработки: локальная сборка вместо релиза).
2. Иначе целевой путь `%LOCALAPPDATA%\kompas-mcp\<version>\kompas-mcp.exe`; существует → запуск.
3. Отсутствует → скачать ассет во временный каталог, посчитать SHA256; **несовпадение — отказ с
ненулевым кодом, скачанный файл не запускается**; распаковать во временный каталог и атомарно
переименовать в целевой (устойчиво к параллельному старту двух сессий).
4. `& $exe @args`; stdio наследуется дочерним процессом; код возврата пробрасывается.
1. `$ErrorActionPreference='Stop'`, `$ProgressPreference='SilentlyContinue'`; весь скрипт обёрнут в
`try/catch`, где `catch` пишет причину в stderr и делает `exit 1`.
2. Задан `KOMPAS_MCP_EXE` → использовать его (цикл разработки: локальная сборка вместо релиза).
3. Иначе целевой каталог `%LOCALAPPDATA%\kompas-mcp\<version>`; установка считается годной, только
если рядом лежит **маркер** `install.json` с совпадающими `version` / `sha256` / `sourceSha`
и присутствуют все обязательные файлы. Одного `Test-Path kompas-mcp.exe` недостаточно —
иначе повреждённая или оборванная установка запускается молча.
4. Установка отсутствует → захватить именованный mutex по версии; после захвата **повторно
проверить** установку (гонку мог выиграть сосед). Скачать во временный каталог
`%LOCALAPPDATA%\kompas-mcp\.tmp\<guid>` — тот же том, иначе переименование каталога не атомарно;
посчитать SHA256; **несовпадение — отказ с ненулевым кодом, скачанное не запускается**;
распаковать, проверить состав, записать маркер, переименовать в целевой каталог. Ошибка
`destination exists` трактуется как успех соседа. Временный каталог удаляется в `finally`.
5. Запуск и проброс кода — точным шаблоном:
**Инвариант:** скрипт не пишет в stdout ни байта (stdout — канал JSON-RPC). Всё — в stderr;
`Write-Host` запрещён.
```powershell
& $exe @args
$serverExit = $LASTEXITCODE
exit $serverExit
```
Публикуется **self-contained win-x64**: другу не нужен установленный .NET 8 Runtime, требуется только
КОМПАС. Цена — ~70 МБ на ассет. Старые версии остаются в кеше рядом — откат сводится к правке
`server.lock.json`.
`powershell -File` возвращает 0, если скрипт не сделал `exit` явно, — без этого падение сервера
останется невидимым.
## 7. Команда `/kompas:doctor`
**Инвариант stdout:** bootstrap-часть не производит **ни одной** строки в success stream (stdout —
канал JSON-RPC). Запрещён не только `Write-Host`: результаты `Get-Item`, `New-Item`,
`ConvertFrom-Json`, `Get-FileHash`, `Expand-Archive` присваиваются или подавляются
(`| Out-Null`); диагностика — только `[Console]::Error.WriteLine()`.
Командный промт (`plugin/commands/doctor.md`), проводящий агента по проверкам с конкретным действием
на каждый отказ:
Артефакт: **self-contained win-x64**, плоская структура ZIP (EXE в корне архива, без `publish/`),
обязательный состав фиксируется списком в спеке реализации. Другу не нужен .NET 8 Runtime, нужен
только КОМПАС. Цена — ~70 МБ на ассет.
1. `server.lock.json` — какая версия ожидается;
2. наличие `%LOCALAPPDATA%\kompas-mcp\<version>\kompas-mcp.exe``KOMPAS_MCP_EXE`, если задан);
3. запущен ли процесс КОМПАС;
4. отвечает ли инструмент `kompas_status`;
5. итоговый отчёт.
Мелкая задача в сервер: флаг `--version`, печатающий версию в stdout и выходящий, — даёт `doctor`
сверку установленного бинаря с локом.
## 8. CI (Gitea Actions в `kompas3d-mcp`)
## 8. Команда `/kompas:doctor`
**`ci.yml`** — push/PR в `main`: `dotnet build -c Release` + `dotnet test --filter Category=Unit`.
Командный промт (`plugin/commands/doctor.md`) с конкретным действием на каждый отказ:
1. ожидаемая версия из `server.lock.json`;
2. маркер `install.json` в `%LOCALAPPDATA%\kompas-mcp\<version>`: совпадают ли `version`/`sha256`;
3. `kompas-mcp.exe --version` против лока (когда флаг появится);
4. переопределение `KOMPAS_MCP_EXE`, если задано;
5. запущен ли процесс КОМПАС;
6. отвечает ли инструмент `kompas_status`;
7. итоговый отчёт.
## 9. CI (Gitea Actions в `kompas3d-mcp`)
**`ci.yml`** — push/PR в `main`: `dotnet build -c Release` + `dotnet test --filter Category=Unit`;
дополнительно проверка согласованности `plugin/server.lock.json` и `plugin/.claude-plugin/plugin.json`
(версии совпадают, `sha256` непустой).
**Интеграционные тесты в CI не выполняются никогда** — им нужен запущенный КОМПАС с GUI и лицензией.
Это граница, а не задача на будущее; фиксируется в README и в workflow комментарием.
Это граница, а не задача на будущее.
**`release.yml`** — на тег `v*`:
**`release.yml`** — на тег строго по шаблону `^v\d+\.\d+\.\d+$`, с `concurrency`-группой на весь
workflow (параллельные релизы сериализуются):
1. `dotnet publish -c Release -r win-x64 --self-contained` → zip → sha256;
2. создание релиза и загрузка ассета через Gitea API (`curl` + `secrets.GITEA_TOKEN`);
3. бот-коммит в `main` с пометкой `[skip ci]`: обновить `plugin/server.lock.json`
(version/url/sha256) и `version` в `plugin/.claude-plugin/plugin.json`.
1. `dotnet publish src/Kompas.Mcp.Host/Kompas.Mcp.Host.csproj -c Release -r win-x64
--self-contained true -p:EnableWindowsTargeting=true -o <dir>` → zip → sha256;
2. проверка состава архива и того, что EXE — PE x64;
3. **upsert** релиза и ассета через Gitea API (`curl` + `secrets.GITEA_TOKEN`): повторный запуск на
том же теге не должен падать на «уже существует»; при совпадении SHA256 загрузка пропускается;
4. скачать ассет по итоговому публичному URL и **повторно сверить SHA256** — только после этого
публиковать указатель;
5. отказ, если версия тега не больше версии в текущей `dist`;
6. сформировать коммит в ветку `dist` поверх отмеченного тегом коммита: `server.lock.json`
(version/sourceSha/url/sha256) и `version` в `plugin.json`.
Требования к раннеру (готовится в отдельной сессии «LXC для Gitea runner»): .NET 8 SDK, доступ к
nuget.org, `curl` и `git`, токен Gitea с правами на релизы и запись в репозиторий. Если раннер
Linux — сборка с `-p:EnableWindowsTargeting=true` (ожидается, что проект соберётся: WinForms/WPF не
используются; **подлежит проверке первым же прогоном**, при неудаче — Windows-раннер).
nuget.org, `curl` и `git`, токен Gitea с правами на релизы и запись в репозиторий.
## 9. Каталог и установка
Cross-publish с Linux ожидаемо работает (`EnableWindowsTargeting` подтягивает Windows targeting/runtime
packs; interop подключены как обычные ссылки, `ole32`/`oleaut32` разрешаются только в рантайме), но
Microsoft рекомендует финальный релиз собирать на Windows, поэтому **перед объявлением релиза годным
выполняется ручной smoke-run на Windows** (см. §12). Первым прогоном отдельно проверить, что
`HintPath` с обратными слешами из `Directory.Build.props` разрешается на Unix.
## 10. Каталог и установка
В `home-repo-cc/.claude-plugin/marketplace.json` добавляется одна запись, один раз:
@@ -152,23 +236,35 @@ Linux — сборка с `-p:EnableWindowsTargeting=true` (ожидается,
{ "name": "kompas",
"source": { "source": "git-subdir",
"url": "https://git.shahovalov.ru/mikhail/kompas3d-mcp.git",
"path": "plugin", "ref": "main" },
"path": "plugin", "ref": "dist" },
"description": "КОМПАС-3D через MCP: построение деталей, сборки, чертежи, STEP",
"category": "cad" }
```
`ref: main`, а не тег: версия для Claude Code берётся из `plugin.json.version`, поднятого релизным
коммитом, поэтому обновления доезжают через `/plugin marketplace update` сами и каталог руками больше
не правится. Каталог приватный, плагин тянется из публичного репозитория — знакомому нужен доступ
только к каталогу.
Каталог приватный, плагин тянется из публичного репозитория — знакомому нужен доступ только к каталогу.
Путь пользователя: доступ в Gitea → `/plugin marketplace add https://git.shahovalov.ru/mikhail/home-repo-cc.git`
`/plugin install kompas@home-repo-cc``/kompas:doctor`.
Установка: доступ в Gitea → `/plugin marketplace add /home-repo-cc.git` →
`/plugin install kompas@home-repo-cc` → `/kompas:doctor`.
Обновление — **три шага, документируются явно**: `/plugin marketplace update home-repo-cc` →
`/plugin update kompas@home-repo-cc` → при необходимости `/reload-plugins`. Полагаться на фоновое
автообновление для приватного HTTPS-каталога нельзя (credential-helper в фоне отключён). Знакомым
рекомендуется **SSH-ремоут каталога** (фоновые pull'ы аутентифицируются ключом из `ssh-agent`); при
HTTPS — выставить `CLAUDE_CODE_PLUGIN_KEEP_MARKETPLACE_ON_FAILURE=1`, чтобы неудачный фоновый pull
не сносил клон.
Предпосылки в `plugin/README.md`: Windows x64; установленный и **запущенный** КОМПАС-3D (проверено на
v24 Home); работа только на одной машине с КОМПАС (сервер — COM-клиент, не автономный CAD-движок).
v24 Home); работа только на машине с КОМПАС (сервер — COM-клиент, не автономный CAD-движок);
репозиторий/чекаут вне OneDrive-дерева.
## 10. Задел под Codex и opencode
Жизненный цикл кеша (документируется, чтобы не пугало): Claude Code хранит каждую версию плагина в
отдельном каталоге и держит осиротевшую примерно 14 дней; активная сессия продолжает работать со
старым `CLAUDE_PLUGIN_ROOT` до `/reload-plugins`, поэтому старая и новая сессии могут одновременно
поднять серверы разных версий из разных каталогов `%LOCALAPPDATA%\kompas-mcp\<version>`. Сам
`%LOCALAPPDATA%\kompas-mcp` **не очищается** ни при обновлении, ни при удалении плагина — чистка
вручную.
## 11. Задел под Codex и opencode
Ни Codex, ни opencode не знают ни `${CLAUDE_PLUGIN_ROOT}`, ни маркетплейсов: для них модель —
клон репозитория и абсолютный путь. Задел выражается не декларацией, а тем, что контент не копируется:
@@ -179,29 +275,51 @@ v24 Home); работа только на одной машине с КОМПА
- лаунчер параметризуется только `server.lock.json`, навыки лежат в одном месте — новый харнесс стоит
README и сниппета, а не форка контента.
Граница явная: это сниппеты и инструкция, а не дистрибутивы. Полноценные плагины Codex/opencode —
отдельная веха, когда у них стабилизируется формат пакета.
Граница явная: это сниппеты и инструкция, а не дистрибутивы.
## 11. Риски и открытые вопросы
## 12. Риски и открытые вопросы
- **Приватный каталог** требует у знакомого аккаунта в Gitea и настроенного git-credential-helper;
что Claude Code корректно клонирует приватный marketplace по https — проверить на живом человеке.
- **Скачивание исполняемого файла** закрыто sha256-пином в репозитории; при несовпадении — отказ.
- **Лицензионный статус interop-DLL АСКОН** при публичности плагина и self-contained-сборки —
открытый вопрос, требует отдельного решения до выхода за круг знакомых.
- **Рекурсия CI**: бот-коммит релиза помечается `[skip ci]`; проверить, что раннер это уважает.
- **Сборка `net8.0-windows` на Linux-раннере** — ожидаемо работает с `EnableWindowsTargeting`,
но не проверена.
- **Расход диска**: каждая версия сервера ~70 МБ в `%LOCALAPPDATA%`; очистка старых версийвручную.
- **Право на редистрибуцию interop-DLL АСКОН — блокирующий вопрос до первого релиза**, а не до
публичности: self-contained ассет уже раздаётся знакомым. Если право не подтверждается, interop в
ассет не включается, и лаунчер должен брать разрешённые сборки из локальной установки SDK — это
меняет модель дистрибуции, поэтому выясняется первым делом.
- **Приватный каталог** требует у знакомого аккаунта в Gitea и настроенных кредов; фоновое
автообновление по HTTPS ненадёжно (см. §10) — проверить на живом человеке.
- **Скачивание исполняемого файла** закрыто sha256-пином в репозитории и повторной сверкой после
публикации.
- **Сборка `net8.0-windows` на Linux-раннере** не проверена; страховка — ручной Windows smoke-run
перед объявлением релиза годным, при провале — Windows-раннер.
- **Расход диска**: каждая версия сервера ~70 МБ в `%LOCALAPPDATA%`, чистка вручную.
- Ревью Codex сняло пункт про «рекурсию CI»: `release.yml` висит только на теге, бот-коммит его не
запускает; на `[skip ci]` полагаться не нужно.
## 12. Критерии приёмки
## 13. Критерии приёмки
1. `tools/sync-agent-assets.ps1` отрабатывает на чистом клоне: навыки видны и в Claude Code, и в
`.agents/skills`, при этом файлы физически существуют в одном месте.
2. Тег `v*` даёт релиз в Gitea с ассетом и обновлённые `server.lock.json` / `plugin.json` в `main`.
3. На машине без предустановленного сервера: `/plugin install kompas@home-repo-cc` → первый запуск
скачивает бинарь, `/kompas:doctor` зелёный.
4. Порча sha256 в `server.lock.json` приводит к отказу запуска с внятным сообщением в stderr.
5. Ручная проверка канала: лаунчер получает `initialize` на stdin и отдаёт корректный JSON-RPC-ответ,
в stdout нет посторонних строк.
6. По playbook'у навыка `kompas:kompas-3d` строится деталь на свежей установке.
1. `tools/sync-agent-assets.ps1` на чистом клоне: навыки видны и в Claude Code, и в `.agents/skills`,
файлы физически существуют в одном месте; при невозможности junction — внятная ошибка, не копия.
2. `git status` чист после sync: содержимое junction'ов не попадает в индекс.
3. Тег `v*` даёт релиз с ассетом и коммит в `dist` с согласованными `server.lock.json` / `plugin.json`;
повторный запуск на том же теге проходит без падения.
4. Холодная установка: `/plugin install kompas@home-repo-cc` → первый запуск качает бинарь →
`/kompas:doctor` зелёный.
5. Две параллельные холодные установки (две сессии одновременно) обе стартуют успешно, каталог
версии не повреждён.
6. Обрыв загрузки и порча `sha256` в локе приводят к отказу с внятным сообщением в stderr и
отсутствию целевого каталога; повторный запуск восстанавливается.
7. Ненулевой код выхода сервера доходит до Claude Code (сервер не выглядит «успешно завершившимся»).
8. Канал: лаунчер получает `initialize` на stdin и отдаёт корректный JSON-RPC-ответ; проверяются
Unicode-содержимое, крупный ответ, поведение при EOF/shutdown, отсутствие осиротевшего
`kompas-mcp.exe`; в stdout нет посторонних строк.
9. Обновление во время активной старой сессии: старая сессия продолжает работать, новая поднимает
новую версию.
10. Windows smoke-run бинаря, собранного на Linux-раннере, перед объявлением релиза годным.
11. По playbook'у навыка `kompas:kompas-3d` строится деталь на свежей установке — навык не
ссылается на отсутствующие в плагине ресурсы.
## 14. Что из ревью отклонено
- **Защита от двух параллельных тегов через CAS/rebase-push и строгую монотонность semver в общем
виде.** У репозитория один мейнтейнер; достаточно `concurrency`-группы, шаблона тега и отказа при
неувеличивающейся версии. Полная машинерия — цена без сценария.
- **Обязательный Windows smoke-run как автоматический гейт релиза.** Windows-раннера нет и он не
планируется; заменён ручным пунктом приёмки (§13.10).