План: скиллы в админском чате — UX и проверяемость

Дата: 2026-08-01. Основано на разведке боем, не на предположениях: три сценария прогнаны через живой /api/agent-chat под ботом agent@docsbook.io (uid 36) на локальной авторизованной превью, плюс аудит кода чата.

Главный вывод разведки, который меняет постановку. Скилл-флоу в чате уже построен end-to-end и неплохо: полный каталог из 52 скиллов инъецируется в системный промпт, есть find_skill/read_skill, /skill-name в композере, галерея выбора через ask_user, follow-up чипы, ведущие по шагам скилла, гейт NEED_SKILL_SELECTION. Строить «интеграцию скиллов в чат» с нуля не нужно — она есть.

Ломается не связка, а три вещи вокруг неё: агент не рассказывает о своём арсенале, не доводит запущенный скилл до результата, и не показывает результат так, чтобы ему можно было верить. Всё три подтверждены прогонами ниже.


1. Что показала разведка боем#

Три прогона через /api/agent-chat (admin mode, ws 141 — единственный dev-воркспейс с живым центовым кошельком на момент прогона).

Прогон 1 — discovery. «Что ты умеешь для моей документации?»#

Ожидание из промпта: каталог инъецирован как «your arsenal», в нём 52 скилла с именами и планами.

Факт: 7 общих категорий, ни одного имени скилла, ни одного вызова тула.

«1. Generate Documentation … 3. Content Improvements: Analyze and improve your existing documentation's content for clarity, SEO, and engagement …»

Админ уходит с ответом «умею улучшать контент» вместо «вот /docs-seo, /docs-health-triage, /docs-change-impact — какой запускаем». Арсенал в промпте есть, наружу не выходит.

Прогон 2 — execution. /docs-seo run it on my docs#

Ожидание из промпта (route.ts:308): «If the user's message already contains a /skill-name token, skip find_skill ranking, read_skill that name directly, and execute.»

Факт — цепочка из 9 вызовов:

find_skill → read_skill → get_workspace_info → get_navigation →
get_workspace_info → set_option → get_navigation →
get_workspace_info → get_workspace_info → ERROR, текста ноль

Четыре дефекта в одном прогоне:

  1. find_skill вызван вопреки инструкции. Явный /docs-seo не сработал как shortcut.
  2. Скилл не выполнен. docs-seo — это аудит SEO: заголовки, мета-описания, структура. Ни одной страницы не прочитано. Вместо работы — цикл из 4× get_workspace_info и 2× get_navigation.
  3. set_option — мутация настроек в audit-скилле. У docs-seo в каталоге mode: audit, он обязан только читать и отчитываться. Режим объявлен в метаданных, но ничем не удерживается в рантайме.
  4. Ноль текста пользователю. Поток кончился ошибкой. Админ 26 секунд смотрит на крутилку и получает пустоту.

Прогон 3 — блокировка на пустом кошельке#

Дальнейшие прогоны упёрлись в token_limit_reached — и это корректная работа гейта (runAgentLoop.ts:356-370): у dev-воркспейсов balance_monthly_cents = 0. Экран показывает «$0.00 LEFT» честно.

Побочно подтвердилась известная грабля: grep без -a молча не нашёл строку "Token budget exhausted for this workspace.", которая лежит в runAgentLoop.ts:366 — файл содержит NUL-байт. Диагностика ушла в ложную сторону на несколько шагов.

Что видно на экране (скриншот пустого чата)#

  • 9 пресетов написаны на языке задач — «Audit my docs», «Check docs SEO», «Find documentation gaps». За каждым стоит скилл, но связь нигде не показана: админ не узнаёт, что нажал скилл, и не научается, что их 52.
  • Весь каталог свёрнут в кнопку «+» без подписи.
  • Дрейф реестра, измеренный фактом: 37 записей в src/components/chat/skill-meta.ts против 52 скиллов в каталоге. 15 скиллов — включая все, написанные за две последние сессии (docs-change-impact, docs-trust-audit, docs-pricing-consistency, docs-competitor-gap, docs-health-triage, docs-rank-recovery, docs-title-rewriter, docs-buying-blockers) — идут без иконки и без примеров вопросов. Файл сам себя предупреждает о дрейфе (комментарий на :44-50), но стража нет.

2. Диагноз: три разрыва, а не один#

# Разрыв Где именно Цена
A Арсенал не виден. Каталог в промпте, но агент говорит о себе абстракциями; UI прячет 52 скилла за «+» и не связывает пресеты со скиллами системный промпт buildAdminSystemPrompt; DocsbookChatInput.tsx; skill-meta.ts (дрейф 37/52) Админ не знает, что покупает. Платящая аудитория пользуется одной десятой продукта
B Скилл не доводится до результата. /skill-name не работает как shortcut; шаги SKILL.md ничем не удерживаются; циклы; audit-скилл мутирует настройки route.ts:308 (инструкция без механики), runAgentLoop.ts (нет step-трекинга, REPEAT_LIMIT=2 не спас) Запуск скилла = лотерея. Это хуже, чем отсутствие скилла: доверие тратится один раз
C Результат недоказуем. Нет отчёта, нет «что изменилось», нет замера пользы нет слоя вывода скилла; get_page_diff_impact (T7) существует, но чат про него не знает Нельзя ни продать («вот что мы вам дали»), ни улучшить («вот что не сработало»)

3. Что делаем: пять работ по убыванию отдачи#

Порядок не по сложности, а по тому, сколько боли снимает единица работы.

W1 — Удержать выполнение скилла (разрыв B) ✅ СДЕЛАНО 2026-08-01 (85116d94)#

Все три механики отгружены и проверены боем на живом /api/agent-chat.

  • /skill-name — настоящий перехват (src/lib/skills/runtime.ts, route.ts): скилл резолвится по каталогу СЕРВЕРОМ до первого хода модели, тело SKILL.md инъектируется в контекст, find_skill/read_skill физически убираются из схемы тулов на этот ход. Прогон 2 повторён: ранжирования больше нет.
  • Режим удерживается в рантайне: ctx.activeSkill.mode === "audit" → мутирующие тулы отклоняются с внятным сообщением (список имён + семейства по префиксу update_/set_/register_webhook_, чтобы новый мутатор был закрыт по умолчанию). Плюс правило в контекстном блоке: audit-скилл не тянет предложение изменения через отчёт.
  • Шаги ## Workflow парсятся в чек-лист хода (extractWorkflowSteps) — источник для W3.

Найденное сверх плана — петля не сходилась. REPEAT_LIMIT не спасал, потому что модель чередовала РАЗНЫЕ read-тулы, возвращающие одно и то же: каждый вызов выглядел новым, ход в целом не узнавал ничего и умирал на max_iterations с пустым текстом. Добавлено: бюджет бесплодных вызовов (считается и повтор сигнатуры, и повтор РЕЗУЛЬТАТА), по исчерпании — тулы снимаются полностью и ход может только писать текст; один добор на молчаливый ход; max_iterations отдаёт человеческое сообщение вместо голого error.

Приёмка выполнена: прогон 2 даёт ✓ без find_skill · ✓ без мутаций · ✓ повтор ≤2 · ✓ текст 1029-1284 симв. · ✓ done. Побочно: стоимость хода упала с ~9.5¢ до ~6.7¢ — меньше холостых round-trip'ов.

Исходная постановка W1

Самая дорогая поломка: скилл запускается и не доходит. Пока это так, улучшать discovery — значит приводить больше людей к сломанному аттракциону.

Три механики, каждая проверяемая:

  1. /skill-name как настоящий shortcut, а не просьба к модели. Сейчас это строчка в промпте, которую модель проигнорировала. Сделать перехват на сервере: токен /name в последнем сообщении → fetchSkillBody(name) вызывается кодом до первого хода модели, тело SKILL.md кладётся в контекст, find_skill из набора тулов на этот ход убирается. Инструкцию, которую модель может не исполнить, заменяем состоянием, которое она не может обойти.

  2. План шагов как состояние хода. После read_skill извлекать нумерованные шаги секции ## Workflow и вести по ним явный чек-лист в контексте: какой шаг текущий, какие закрыты. Это же — источник для UI-прогресса (W3). Без него длинный скилл разваливается в свободное блуждание, что прогон 2 и показал.

  3. Режим скилла удерживается в рантайме. У каждого скилла есть metadata.mode (audit / refactor / authoring / platform) — и он сейчас чисто декоративный. Пока активен audit-скилл, мутирующие тулы (set_option, update_*, commit_docs, write_docs) должны отклоняться с внятным «этот скилл только читает; чтобы применить — выйдите из аудита». Прогон 2 показал set_option внутри docs-seo — это не теория.

Приёмка: прогон 2 повторяется и даёт: без find_skill, без повторов одного тула подряд, без мутаций, с непустым финальным текстом.

W2 — Тесты, которые ловят именно это ✅ СДЕЛАНО 2026-08-01 (85116d94)#

Харнесс: scripts/chat-scenarios/ (run.mjs + setup-workspace.mjs + README).

  • 6 сценариев, каждый написан на реально наблюдавшийся дефект, а не воображаемый: discovery-names-skills, slash-shortcut, audit-readonly, metric-routes-to-tool, skill-reaches-data, no-plan-assumption.
  • Детерминированные чеки: какие тулы вызваны, каких быть НЕ должно, no_mutations по семействам, максимум повторов подряд, непустой текст, поток кончился done, и — добавлено по факту прогонов — no_trailing_promise (ход не должен кончаться фразой «сейчас соберу данные»).
  • LLM-судья получает инструкцию ИЗ промпта + фактический ответ и отвечает на закрытый вопрос с цитатой.
  • A/B на каждый чек выполнен: подсадка дефекта роняет ровно тот чек, что должен (снял префиксную защиту мутаторов → упал isMutatingTool; разрешил вложенные шаги → упали 4 чека парсера; удалил запись из skill-meta → упал страж).
  • Изолированный воркспейс с бюджетомsetup-workspace.mjs часть харнесса: выдаёт план pro_plus и живой кошелёк, и указывает на репо С ДОКУМЕНТАЦИЕЙ (пустой репо заставлял агента метаться между get_navigation/get_workspace_info — выглядит как баг агента, а им не является).
  • Стоимость печатается из того же кошелькового леджера, на котором продукт биллит: ~6-10¢ на сценарий, ~40-60¢ полный набор. Решение принято по цифре: детерминированные чеки (--no-judge) на каждый пуш, судья — по расписанию.

Что тесты поймали сверх ожидаемого: ответ, заканчивающийся обещанием вместо результата (устойчиво, 2 прогона подряд) и допущение о плане пользователя («продолжу, предполагая что у вас Pro»). Оба закрыты правилами в промпте.

Урок про сами тесты (риск №1 подтвердился): первая версия plan-block-degrades судила НАЛИЧИЕ блокировки — а это свойство фикстуры, не агента, поэтому чек мигал. Переписан в no-plan-assumption: судит то, что обязано держаться при любом плане. Второй случай: судья audit-readonly флапал на формулировке — вопрос сужен до «заявлено ли изменение сделанным».

Исходная постановка W2

Формулировка Dan: «что действительно ИИ возвращает логичный ответ исходя из ожиданий промпта». То есть проверяем не «не упало», а соответствие поведения тому, что промпт обещает.

Устройство: сценарий = { вход, ожидания }, где ожидания двух сортов.

Детерминированные (без LLM, дёшево, в CI):

  • какие тулы вызваны и в каком порядке;
  • каких тулов быть НЕ должно (для audit-скилла — весь мутирующий набор);
  • нет повторов одного тула подряд больше N;
  • финальный текст непустой и длиннее порога;
  • поток закончился done, а не error.

Это ловит ровно прогон 2 и стоит один прогон чата.

LLM-judge (там, где детерминизм невозможен): судья получает инструкцию из промпта и фактический ответ и отвечает на конкретный вопрос, а не «хорошо ли». Например: «Промпт называет каталог скиллов арсеналом агента. Названы ли в ответе конкретные скиллы по именам? Да/нет + цитата». Это ловит ровно прогон 1.

Три обязательных свойства, иначе тесты станут украшением:

  • A/B на каждый чек. Чек засчитывается, только если падает на подсаженном дефекте. Иначе получится набор, который всегда зелёный. (Ровно так проверялась контрольная группа в T7 — подсадка роняла 2 теста.)
  • Изолированный воркспейс с живым кошельком. Разведка встала на balance_monthly_cents = 0 у всех dev-воркспейсов. Тестам нужен свой, с бюджетом, и его пополнение — часть сетапа, а не ручная операция.
  • Стоимость видна. Каждый прогон печатает потраченные центы. Набор из 20 сценариев на каждый пуш может стоить дороже пользы — это решение принимается по цифре, а не на глаз.

Первые 6 сценариев (по одному на найденный дефект + покрытие ключевых режимов):

  1. discovery: «что ты умеешь» → в ответе ≥3 имени скилла;
  2. /docs-seo → нет find_skill, нет мутаций, есть текст;
  3. audit-скилл + просьба «поменяй настройку» → вежливый отказ, а не мутация;
  4. /docs-change-impact → доходит до вызова аналитики, не выдумывает числа;
  5. вопрос про метрику («сколько визитов вчера») → идёт в find_tool, а не в скилл (маршрутизация из route.ts:221);
  6. free-план + PRO-скилл → внятная деградация с названием плана, а не падение.

W3 — Показать, что скилл делает и что сделал (разрывы B и C)#

UI сейчас показывает поток тулов строками. Скилл — это не поток тулов, это процедура с шагами и результатом.

  • Прогресс по шагам: когда активен скилл, в треде видно «Шаг 2 из 6: собираю заголовки страниц» — данные берутся из плана шагов (W1.2). Это же снимает «26 секунд крутилки».
  • Отчёт как артефакт, а не простыня. У audit-скиллов результат структурен по своей природе (находка, доказательство, серьёзность). Отдавать его карточкой с находками и ссылками на страницы, а не абзацем текста. Механика для этого уже есть — admin-cards + render_widget_card.
  • Явный конец. Скилл закончился → одна строка «что нашли / что изменили / что дальше». Сейчас конца нет вовсе: поток просто останавливается.

W4 — Сделать арсенал видимым (разрыв A)#

Частично СДЕЛАНО 2026-08-01 (7d56ccbe): «Try Docsbook» переписан из сетки 9 карточек в листаемую библиотеку из 57 промптов.

  • Карточка = результат, не скилл. Один живой вопрос («почему у меня не апгрейдятся?») законно тянет несколько скиллов, и выбирать — работа агента, а не кнопки. Поэтому ни одна карточка скилл не называет; библиотека разложена по бизнес-осям (acquisition / activation / conversion / retention / build).
  • Три длины под три задачи: заголовок в 2-3 слова (влезает в карточку), описание одной строкой на ховере (оно же нативный тултип и доступное имя), и длинный промпт под результат, который уходит агенту. Проверено перехватом: карточка из 14 символов отправляет 270.
  • Страницы 3×3 со счётчиком «2 / 4», стрелками и свайпом. Перемешивание раз на монтирование с чередованием по осям — каждое открытие показывает другую грань продукта, и страница не оказывается девятью вариациями одного.
  • Проверено в браузере на всех 4 страницах: не обрезается ничего (два заголовка ловились переполнением и укорочены), ховер заполняет строку в шапке, листание закольцовано.

Ещё две трети СДЕЛАНО 2026-08-01 (85116d94):

  • Дрейф skill-meta.ts закрыт и заперт. Было 37 записей против 52 скиллов — дописаны все 15 недостающих (иконка + короткий лейбл + примеры вопросов), и поставлен тест-страж src/components/chat/__tests__/skill-meta-drift.test.ts: каждый скилл каталога обязан иметь запись, устаревшая запись без скилла тоже красная, лейбл ≤30 символов (длиннее — обрезается в меню), ≥2 примера. При недоступной сети тест СКИПАЕТСЯ, а не падает: красный, означающий «нет вайфая», приучает игнорировать красный. A/B: удаление одной записи роняет стража.
  • Правило «отвечай именами» в промпте. Сначала было вписано в раздел про скиллы — не сработало (прогон дал 0 имён). Перенесено в НАЧАЛО промпта с явным форматом (- **\/docs-seo`** — строчка про результат) и запретом строк без имени скилла. После переноса: 3 прогона подряд по 3-5 имён, судья yes`.

Осталось по W4: пресеты в «+»-меню называют скилл; каталог как страница внутри чата.

  • Пресеты называют скилл. «Audit my docs» → показывать, что это /docs-analyze. Админ учится языку системы, а не угадывает.
  • Каталог как страница внутри чата (дешёвая версия: расширенное «+»-меню с группировкой по бизнес-оси — привести людей / довести до ценности / продать / удержать, как в плане каталога). Полноценную галерею — только если после W1–W3 останется, что показывать.

W5 — Замерить пользу (разрыв C, самое медленное)#

Здесь пригождается T7 get_page_diff_impact, написанный в прошлой сессии: он умеет отвечать, помогла ли конкретная правка, сравнивая изменённые страницы с нетронутыми.

Петля: скилл предложил правку → правка закоммичена → через окно измерения чат сам возвращается: «docs-title-rewriter переписал 6 заголовков две недели назад; на этих страницах отказы упали на 9 пунктов против 2 по остальному сайту». Это одновременно доказательство пользы продукта и вход в следующую работу.

Осторожно: T7 честно отказывается судить на малых выборках (проверено боем: 22 и 8 визитов → «not enough visits to judge»). У большинства клиентских воркспейсов трафик именно такой. Значит петля даёт результат не всем и не сразу — обещать «мы измерим эффект» в UI нельзя, можно только показывать, когда измерение получилось.


4. Порядок и почему такой#

  1. W1 ✅ — без него остальное усиливает поломку.
  2. W2 ✅ — сразу за W1, потому что W1 иначе нечем принять. Тесты пишутся на найденные дефекты, а не абстрактные.
  3. W3 👈 ТЕПЕРЬ ПЕРВЫЙ — делает работу W1 видимой; переиспользует её план шагов (extractWorkflowSteps уже парсит их и кладёт в контекст, до UI дело не дошло).
  4. W4 — приводит людей к тому, что теперь работает. Дрейф skill-meta и правило «отвечай именами» уже закрыты; остались пресеты со скиллом и каталог-страница.
  5. W5 — отдельный горизонт, зависит от накопления данных.

W1 + W2 сделаны одним заходом — и это оправдалось: три из пяти правок W1 родились из провалов сценариев, а не из плана (несходимость петли на разных read-тулах, ответ-обещание, допущение о плане). Принимать W1 на глаз означало бы отгрузить перехват /skill-name и не заметить, что ход по-прежнему умирает пустым.


5. Риски#

  • Тесты на LLM недетерминированы by design. Один прогон ничего не доказывает. Либо несколько прогонов и порог «k из n», либо чек формулируется так, чтобы шум его не двигал. Иначе красный CI станет фоном, который перестанут читать.
  • Стоимость. Полный набор на каждый пуш может оказаться дороже пользы. Решение — по измеренной цифре: дешёвые детерминированные чеки на каждый пуш, LLM-judge — по расписанию.
  • Жёсткое удержание шагов может сломать полезную гибкость. Часть скиллов сознательно оставляет агенту свободу. Удерживать надо режим и факт завершения, а не каждый шаг буквально.
  • Локальная среда не воспроизводит планы. В dev-БД все воркспейсы бота — free с нулевым кошельком. PRO-поведение (а большинство аналитических скиллов — PRO) локально не проверяется вообще. Это та же грабля, что в growth-audit с ролью Free. Тестовому контуру нужен воркспейс с планом и бюджетом, иначе половина сценариев непроверяема.
  • skill-meta.ts разъедется снова. Без стража в CI — гарантированно; он уже разъехался на 15 скиллов при наличии предупреждающего комментария в самом файле.

6. Что уже проверено и не требует повторной разведки#

  • Скилл-флоу существует и вызывается: find_skillread_skill отработали в прогоне 2.
  • Каталог тянется в промпт server-side с кешем (listSkillCatalog, src/lib/skills/find.ts:214).
  • /skill-name доезжает до сервера и снимает гейт NEED_SKILL_SELECTION, но не работает как shortcut.
  • Гейт бюджета в админ-чате есть и корректен (runAgentLoop.ts:356).
  • Агент не ходит в свой MCP-сервер: у чата отдельный набор тулов, переиспользуется бизнес-логика. Точка переиспользования для скиллов — src/lib/skills/find.ts.
  • Новые тулы MCP (в т.ч. get_page_diff_impact) в чат-агенте не появляются автоматически — их набор отдельный. Для W5 тул придётся завести и в чате.

Добавлено после реализации W1+W2 (2026-08-01)#

  • metadata.mode живёт в двух местах: в index.json он на верхнем уровне (mode), в самом SKILL.md — под metadata.mode. Правда — второе (его правит автор скилла), runtime.ts читает body и падает на индекс. Распределение: audit 28, platform 11, authoring 8, refactor 5.
  • raw_url скиллов НЕ содержит категорию единообразно: у одних skills/<category>/<name>/SKILL.md, у других skills/<name>/SKILL.md. Не строй путь сам — бери raw_url из индекса.
  • Пустой воркспейс делает сценарий непроверяемым и врёт про причину. Свежесозданный воркспейс указывал на несуществующий репо — агент метался между get_navigation/get_workspace_info 14 раз. Выглядит как петля агента, а на деле нечего читать. Фикстура обязана указывать на репо с документацией.
  • Разведка была права про кошельки, но не про дно: у бота есть воркспейсы с ненулевым балансом (139/143/146/150/151), просто мало. setup-workspace.mjs поднимает выбранный до pro_plus + $5.
  • Дев-логин ботом воспроизводится из curl ровно как в proxy.ts: GET /api/auth/csrf → POST /api/auth/callback/agent-bypass с csrfToken+secret. Это и есть механика харнесса, браузер не нужен.
  • no_trailing_promise ловится дёшево регэкспом — судья для этого не нужен: «let me now gather…», «please hold», «get back to you» в хвосте ответа.