Отладочный механизм: субагент строит модель ТОЛЬКО инструментами MCP и по навыкам,
без подсказок вызывающего, а затем сдаёт протокол — вызовы, затыки, цитаты из навыка,
чего не хватило в каталоге. Пишущих инструментов у него нет намеренно: нехватка
возвращается находкой, а не самодельным обходом. Проектный, в плагин не входит.
kompas-3d: раздел «Два пути формообразования — выбирай по форме, а не по
привычке» (призматика и карманы примитивами, текст и кривые эскизами) и
раздел про надписи с числами, снятыми на практике: height задаёт высоту
ПРОПИСНОЙ, а не габарит строки; разрыв между advance-длиной и габаритом
глифов зависит от шрифта (1-2 % у наборных, 7-9 % у скриптовых) — прежние
формулировки были выведены из одного замера и оказались неверны.
Там же — почему тонкая стенка по многоконтурному эскизу не строится поверх
пересекаемого тела (разложено экспериментом: число тел, происхождение тела
и chooseType ни при чём) и как скруглять рельефный текст: по одному ребру
строится, пакетом по всем 203 — нет.
CLAUDE.md и kompas-mcp-dev: спайк-проект под кейс допустим для проверки
теории, но итоговая деталь всегда строится инструментами MCP — раннер
обходит тот самый слой, который мы проверяем.
set_part_info задаёт наименование и обозначение детали: без него в дереве
стоит безликая «Деталь», и она же уходит в штамп чертежа и в спецификацию.
describe_model показывает наименование в шапке, а безымянную деталь помечает
явно. Имя файла этого не заменяет — это разные свойства.
document_save рапортовал «Сохранено», ничего не записав: SaveAs возвращает
void и молча отказывает, если файл с этим именем уже открыт в КОМПАС другим
документом. Теперь путь и наличие файла проверяются после записи, а Save
сверяет флаг Changed. Освободить занятое имя нечем не было — document_close
получил режим all.
Текст исключения доходил до клиента только у McpException, всё остальное
подменялось на «An error occurred invoking». То есть подсказки в наших
сообщениях агент не видел вовсе. Фильтр CallTool в Program.cs возвращает
IsError с реальной причиной (ToolErrorText разворачивает AggregateException
и склеивает вложенные причины). Там же — регистрация PrimitiveService.
Три способа получить форму там, где раньше был только замкнутый контур
из отрезков и дуг.
1. Примитив эскиза type=text: ksTextEx + ksConvertTextToCurve. Пока текст
остаётся текстом, это оформление, и операция его не видит; после
конвертации глифы становятся обычным сечением. Ответ возвращает
фактическую длину строки — иначе ширину шрифта до выдавливания не узнать.
2. extrude(thinThickness, thinSide): контур трактуется как стенка заданной
толщины. outward по замкнутому контуру даёт кайму-эквидистанту вокруг
него — так надпись получает подложку без 2D-эквидистанты.
Толщину при dtReverse КОМПАС читает из reverseThickness: положить её в
normalThickness значит молча получить СПЛОШНОЕ сечение вместо рамки.
3. Инструмент primitive: элементарные тела API7 (block, cylinder, sphere,
cone) с result=new|union|subtract|intersect. Карман и паз строятся
вычитанием тела, без эскиза и выреза. Update() возвращает TRUE даже
когда вычитание прошло мимо тела, поэтому сервис сверяет объём до и
после и откатывает операцию, если ничего не изменилось.
Get-BlockingProcess возвращает скаляр, когда bin занят ровно одним процессом
MCP, и .Count под Set-StrictMode роняет скрипт вместо диагностики. Та же
ловушка, что описана для лаунчера, — теперь результат заворачивается в @().
Инструменты были нарезаны по способу вызова, а не по смыслу: четыре отверстия,
двойники *_index, одиннадцать sketch_add_*. Агент платил за это дважды — 40 КБ
описаний в каждой сессии и лишние round-trip'ы, а каждый вызов это ещё и шанс
сбиться. Теперь инструмент называет ОПЕРАЦИЮ, вариант задаётся параметром,
объект выбирается индексом или точкой одним и тем же инструментом.
Проверка построения приходит сама. Каждая мутирующая операция дописывает к ответу
итог validate_part (AutoValidation): Create()/Update()==true не значит успех, а
правило навыка «проверяй после каждого шага» удваивало число вызовов. Выключается
через set_auto_validate или KOMPAS_MCP_AUTOVALIDATE=0 — сбой самой проверки уходит
в примечание и никогда не превращает удачную операцию в ошибку.
Эскиз: 16 инструментов → 3. sketch_create(plane|faceIndex|x,y,z, entities[],
autoClose) строит контур целиком; пакет выполняется за ОДИН заход на STA-поток
(PartModeler.AddEntitiesAsync), ошибка называет позицию примитива в списке.
Слияния: hole(type=simple|counterbore|countersink|conic), extrude/revolve(mode),
pattern(kind), mirror (без featureIds — всё тело), document_save(path?),
set_variable как upsert (разведочный вызов «есть ли такая» больше не нужен),
list_faces/list_edges(index?) вместо отдельных describe_*, get_part_info и
get_bounding_box — в describe_model(sections), где незапрошенные разделы вообще
не читаются из модели.
Селектор index|point: fillet_edge/chamfer_edge принимают edgeIndices списком —
одна операция дерева на все рёбра; для операций API7, умеющих только точку,
индекс переводится в точку через ModelInspectionService.FaceCenterPointAsync
(середина параметрической области грани).
Схема слитого инструмента не запрещает неверную комбинацию полей — это делает
валидация, и её сообщение называет type и недостающий параметр.
Тесты: 291 unit (+41) и 136 integration (+5), интеграционные — на живом КОМПАС.
Новые проверяют ровно рискованные места: точка-из-индекса лежит на грани и по ней
создаётся эскиз, мульти-ребёрное скругление даёт один узел дерева и убыль объёма,
upsert создаёт и затем меняет переменную с формулой, пакет сообщает позицию сбоя.
PluginSkillsTests теперь падает, если в публикуемом навыке всплывёт слитое имя.
Пока MCP-сервер работал прямо из src\...\bin, он держал открытым kompas-mcp.exe
и Kompas.Mcp.Core.dll — dotnet build падал с MSB3027 от каждой живой сессии.
Лаунчер копирует сборку в %LOCALAPPDATA%\kompas-mcp\dev\<отпечаток> и запускает
её оттуда, поэтому bin свободен всегда.
- tools/dev/KompasMcpDev.psm1 — отпечаток каталога сборки, теневая копия через
атомарный Directory.Move, уборка старых копий мимо занятых живым процессом,
сессионный mutex на одновременный старт нескольких сессий;
- tools/dev/launch-dev-mcp.ps1 — точка входа для .mcp.json, инвариант «в stdout
до старта сервера не уходит ни байта»;
- tools/dev/rebuild-mcp.ps1 — -Check компилирует мимо bin (~2 с при живом
сервере), обычный режим отказывается заранее и называет блокирующие PID;
- tools/tests/DevLauncher.Tests.ps1 — Pester на все три файла.
Раздел «The rebuild cycle» в CLAUDE.md (2c56511) уже описывал эту схему.
Три канала обратной связи (патч в src, новый общий тул, правка навыков),
правило «навыки отлаживаются как код» с source of truth в plugin/skills,
запрет утечки внутреннего в публикуемые навыки и ловушка с залоченным
kompas-mcp.exe при пересборке.
Корневой CLAUDE.md грузится в каждую сессию, но большая его часть нужна только
при доработке сервера. 37 476 → 12 203 символов (~6 300 токенов на сессию).
Перенесено в .claude/skills/kompas-mcp-dev/reference/:
- com-implementation-facts.md — проверенные COM-цепочки («Key implementation facts»);
- plugin-and-release.md — устройство plugin/, ветка dist, релиз, CI, обновление RAG-базы.
Релизная кухня положена в навык, а не в plugin/CLAUDE.md: plugin/ раздаётся
пользователям, внутренностям сборки там не место. В корневом файле оставлены два
правила, которые нельзя откладывать до загрузки навыка — обязательный бамп версии
в plugin.json и UTF-8 с BOM для PowerShell плагина.
Удалено как выводимое из репозитория: раскладка каталогов, стек и счётчики тестов,
список режимов sync-agent-assets.ps1, перечень содержимого каталога SDK.
Секция «Navigating the SDK base» удалена как дубль §«Порядок поиска» из определения
агента kompas-sdk-research — вдобавок она описывала инструменты, доступные только
субагенту. Уникальная процедура обновления базы перенесена в навык.
Документация переписана под состояние без каталога docs/
Каталог docs/ удалён в b022a91, но CLAUDE.md и README ссылались на
ARCHITECTURE.md, TODO.md, OPEN_QUESTIONS.md, IMPLEMENTATION_PLAN.md и
presentation.html — все ссылки были битыми.
- CLAUDE.md: явно зафиксировано, что прозаическая документация — только он и
README, docs/ воссоздавать не нужно; бэклог (чертёж: базы/допуски/привязка,
сопряжения сверх coincidence и distance) и известный подводный камень с
направлением по нормали грани перенесены из удалённых TODO/OPEN_QUESTIONS
- убран субагент docs-maintainer: он удалён вместе с docs/, доки правятся руками
- ссылка на спеку по параметрическим эскизам заменена на память
- README: структура репозитория приведена к реальной, добавлены --version,
run-ps-tests.ps1 и оговорка про .slnx на SDK 8
- kompas-mcp-dev: справка SDK описана как RAG-база kompas-sdk вместо
несуществующего docs/Kompas3D_SDK/, добавлен порядок работ по новому
инструменту, команды проверки и границы CI
Счётчики сверены по коду: 84 инструмента, 250 unit + 131 integration, 41 Pester.
@
Продолжение 4d4e114 (сборки КОМПАС убраны из поставки).
- kompas_status проверяет интероп до обращения к сессии и отдаёт причину текстом:
иначе MCP SDK показывает агенту только «An error occurred invoking …»
- провайдер сносит негодный остаток кеша перед Move: повтор после оборванной
распаковки падал IOException вместо внятного сообщения
- README/plugin/README/doctor/CLAUDE.md: требование компонента SDK,
KOMPAS_INTEROP_DIR, кеш в %LOCALAPPDATA%, счётчики тестов 381 (250 unit)
finally в Install-KompasMcpServer спасает только от штатного throw/return.
После принудительного убийства процесса посреди установки каталог
.tmp\<guid> оставался навсегда — по ~70 МБ на оборванный запуск, сервер
публикуется self-contained.
Remove-KompasMcpStaleTemp удаляет подкаталоги .tmp старше суток. Возраст
считается как максимум LastWriteTimeUtc по всему поддереву: пока сосед
льёт байты в один и тот же server.zip, время записи самого каталога не
меняется, и по нему можно было бы снести живую установку.
Вызов стоит в начале Install-KompasMcpServer, до раннего выхода по
Test-KompasMcpInstall — иначе мусор от оборванных попыток лежал бы до
следующего релиза. Если каталога .tmp нет, всё сводится к одному
Test-Path.
Функция не бросает исключений и не пишет в success stream: запуск
сервера важнее мусора, а stdout занят JSON-RPC.
При переключении на ветку или коммит без plugin/skills junction'ы навыков
повисают битыми, и git checkout падает на середине: часть рабочего дерева
уже удалена, а HEAD остаётся на прежней ветке.
Remove-AgentSkillLink удаляет сам reparse point через Directory.Delete
(без -Recurse), поэтому в target не заходит и содержимое plugin/skills не
трогает. Junction опознаётся по атрибуту ReparsePoint, а не по резолву
target — иначе режим не работал бы ровно в том случае, ради которого сделан.
Настоящий каталог не удаляется, а возвращает Skipped.
Режимы скрипта разведены parameter set'ами: -Remove -Check вместе теперь
дают ошибку разбора параметров вместо тихого выигрыша одного из ключей.
Покрыто пятью кейсами в tools/tests/AgentAssets.Tests.ps1 (включая junction
с исчезнувшим источником); порядок действий и восстановление после падения
чекаута описаны в CLAUDE.md.
База docs/Kompas3D_SDK/ (2465 статей) переехала в приватный kompas-sdk-docs
и проиндексирована в RAG на CT 127; субагент kompas-sdk-research переведён
на MCP-инструменты сервера kompas-sdk.
Спек дополнен фактами прогона: 2465 документов / 13 755 точек, проверка
grep/search/фильтров, живой прогон CI с инкрементальной переиндексацией.
Отдельно зафиксирована находка — реранк падал 400 на длинных чанках
(слот 1024 токена против обрезки в 2000 символов); починено в rag-node.
Гейт «версия не должна уменьшаться» отвергал и равную версию безусловно, что
делало невозможным повторный прогон на том же теге (Task 10 Step 4 плана).
Теперь сравнение version+sourceSha читается из plugin/server.lock.json ветки
dist: равная версия проходит дальше только если sourceSha из dist совпадает с
текущим github.sha (ретрай уже опубликованного релиза — ниже по пайплайну он
и так идемпотентен: upsert релиза/ассета, повторная сверка sha256, force-push
dist тем же содержимым). Другой sourceSha при той же версии — попытка
подменить уже опубликованный релиз другим кодом — отвергается с явным
сообщением, какой коммит уже занимает эту версию. Меньшая версия отвергается
как и раньше.
ci.yml: сборка + unit-тесты на Linux-раннере (EnableWindowsTargeting), плюс
проверка согласованности plugin/.claude-plugin/plugin.json и
plugin/server.lock.json (версии совпадают; для опубликованного релиза —
sha256 непустой и 64 hex).
release.yml: на тег vX.Y.Z — публикация self-contained win-x64,
идемпотентный upsert релиза/ассета через Gitea API, сверка SHA256
опубликованного ассета перед обновлением, и force-push ветки dist поверх
коммита с тегом (server.lock.json + plugin.json). Отличия от черновика в
плане: устойчивый разбор "версия не должна уменьшаться" (не полагается на
особенность jq на пустом stdin), явные HTTP-коды вместо curl -f для
различения "релиза нет" от прочих ошибок API, persist-credentials: false
на checkout, чтобы не конфликтовать с собственным токеном на пуше.
Не прогонялось на раннере — раннер ещё не готов (отдельная работа).
server.lock.json вида "{}" (валидный JSON, ноль ключей) — реальный вход
(placeholder до заполнения релизным пайплайном), но под
Set-StrictMode -Version Latest член-перечисление .PSObject.Properties.Name
бросает PropertyNotFoundException, когда коллекция .Properties пуста, — Read-
KompasMcpLock падала сырым .NET-трейсом вместо дружественного сообщения.
Тесты этого не ловили: любой объект хотя бы с одним свойством (даже без
version) уже не задевает пустую коллекцию.
Фикс: PSObject.Properties['имя'] (индексатор) вместо .Properties.Name
-contains — он возвращает $null и не бросает независимо от того, пуста
коллекция или нет. Применено ко всем трём проверкам (version/url/sha256).
Отдельно исключён случай $lock -eq $null (JSON "null" и пустой файл
разбираются в $null, а $null.PSObject тоже бросает под StrictMode).
Добавлены тесты: лок "{}", лок "null", лок "" (пустая JSON-строка вместо
объекта) — все дают сообщение с именем файла, а не .NET-трейс. Известные
пробелы покрытия (IOException-гонка вокруг Directory.Move,
AbandonedMutexException) зафиксированы комментарием в тестовом файле — не
автотестируются осознанно (нужен настоящий межпроцессный тайминг).
- Test-Path -PathType Leaf: путь-каталог в KOMPAS_MCP_EXE больше не проходит
проверку молча — раньше падение случалось на голом & $exe сырым
CommandNotFoundException в обход диагностической обёртки.
- запуск сервера (& $exe @args) обёрнут в try/catch: сбой самого запуска
(битый бинарь, недостающая зависимость, отказ в доступе) теперь тоже даёт
чистое сообщение 'kompas-mcp launcher: ...' и код 1, а не сырой трейс;
ненулевой код возврата уже запущенного сервера по-прежнему пробрасывается как есть.
- тесты: три новых сценария (каталог вместо файла, ноль байт в stdout на
пути отказа, повреждённый exe) + первый тест переписан на побайтовую
проверку stdout через Start-Process без фильтрации пустых строк.
Три предметных замечания ревью качества:
- Resolve-KompasMcpExecutable: WaitOne() перенесён в try/finally, отдельно
ловится AbandonedMutexException (владелец умер посреди установки — владение
переходит к нам, это не ошибка); ReleaseMutex вызывается только если mutex
реально захвачен, иначе он бросает SynchronizationLockException и маскирует
первопричину. Добавлен -Downloader passthrough для тестируемости.
- Read-KompasMcpLock: разбор JSON обёрнут в try/catch с сообщением, называющим
файл; наличие полей version/url/sha256 проверяется через
PSObject.Properties.Name (обращение к отсутствующему свойству под
Set-StrictMode -Version Latest бросает PropertyNotFoundException); sha256
валидируется на 64 hex-символа.
- Добавлены тесты на конкурентную логику: Resolve-KompasMcpExecutable
(повторный вызов не скачивает), гонка "сосед выиграл" (целевой каталог уже
содержит годную/негодную установку) в Install-KompasMcpServer, счастливый
путь и все новые ветки валидации в Read-KompasMcpLock.
Мелочи: & $Downloader ... | Out-Null (внедряемый загрузчик не должен писать
в success stream); уточнён комментарий Test-KompasMcpInstall — оно намеренно
трактует любую ошибку чтения маркера (не только битый JSON, но и блокировку
антивирусом) как "установка негодная".
AbandonedMutexException-ветка вручную воспроизведена и проверена: процесс-
владелец mutex убит, пока другой процесс уже блокирован в WaitOne() —
тот получает AbandonedMutexException и корректно продолжает работу.
MD-база docs/Kompas3D_SDK/ (2465 статей, 10.4 МБ) переехала в приватный
kompas-sdk-docs и проиндексирована в RAG на CT 127. Доступ — MCP-сервер
kompas-sdk (порт 8092); субагент kompas-sdk-research переведён с Grep/Read
на его инструменты и теперь ищет по базе, которой в репозитории нет.
Порядок поиска в инструкции агента: grep_knowledge для точных имён,
search_knowledge с фильтрами type/tags для задачных вопросов,
get_chunk_context вместо get_document (геометрия.md весит 261 КБ).
Обновление при новой версии КОМПАС — update_sdk_docs.ps1 в репозитории
доков: zip справки → генератор → diff → тег → push → переиндексация
через Gitea Actions. Спек и фактические результаты —
docs/superpowers/specs/2026-07-31-sdk-docs-rag-migration-design.md.
Прежний комментарий утверждал, что Global\ требует отдельной привилегии —
ревью опровергло это эмпирически (мьютекс создался без ошибок под тем же
непривилегированным пользователем). Настоящая причина: плагин ставится на
чужие незнакомые машины, где политика безопасности может отличаться, и отказ
Global\ был бы фатальным для запуска сервера, тогда как остаточный риск
сессионного mutex (лишняя повторная загрузка при параллельных сессиях)
ограничен и не портит установку.
- AgentAssets.psm1 и sync-agent-assets.ps1 пересохранены в UTF-8 с BOM:
powershell.exe читает файлы без BOM в системной ANSI-кодировке и ломает
парсинг на кириллических строковых литералах (pwsh 7 работал за счёт
автодетекта и маскировал баг).
- Copy-Item в ветке -AllowCopy получил -ErrorAction Stop: без него ошибка
копирования писалась как non-terminating и функция возвращала 'Copied'
при частично скопированном каталоге, если вызывающий не выставил
$ErrorActionPreference='Stop' сам.
- Test-AgentSkillLink сравнивает пути через
[string]::Equals(...,OrdinalIgnoreCase) явно, а не через регистронезависимый -eq.
- Добавлены тесты: отсутствующий SourcePath, перелинковка на другой
источник (старый target остаётся нетронутым) и AllowCopy-регрессия
(мок Copy-Item с [CmdletBinding()], чтобы корректно эмулировать
ErrorAction реального cmdlet).