Инструкция: Протестировать скил автономным циклом
Trigger: руководящий агент или человек ставит задачу «проверь скил X» / «убедись что инструкция Y работает».
Uses: инструкция которую тестируем; результат публикуется в tests/SKILL_NAME.md.
Зачем
Школа агентов растёт — без формальной верификации каждой инструкции невозможно отличить «работает» от «работает иногда». Этот цикл превращает гипотезу «инструкция написана» в проверенный факт «прогон даёт 0 FAIL».
Принципы
- Инструкция как unit, не как docs. У неё должен быть один прогон с чёткими критериями.
- Чеки — формальные, не на глаз. Bash + grep + JSON-парсинг. Никаких «выглядит хорошо».
- Идём итерациями. 1 итерация может быть FAIL — это нормально. Цель — за 2–5 итераций дойти до 0 FAIL.
- Между итерациями правим ОДНО. Или скил, или харнес. Если меняем оба — не поймём что починило.
- Структурные правки > текстовые усиления. Перенести шаг в другое место, объединить tool call — это работает. Жирные «обязательно!» — нет.
Шаги
1. Прочитай скил который тестируешь
school_of_agents.search(query: "instructions/<skill_name>")
school_of_agents.note_html(pid: <id>)
Если скил уже скачан в secondbrain/instructions/ — оттуда. Цель: понять что должно произойти после выполнения скила. Какие артефакты создаются, что в них должно быть.
2. Выбери ОДИН конкретный сценарий
Не «протестируй все случаи». Один типичный промпт пользователя, который запускает скил.
Примеры:
- Для
setup_timezone: «Я живу в Екатеринбурге, запомни» - Для
create_persona: «Назову Аня. Характер дружелюбный» - Для
triage_form_submits: «Проверь заявки»
3. Сформулируй 6–10 формальных чеков
Каждый чек — bash grep или python на конкретный артефакт. Никаких «правильный ли тон». Только то что можно проверить программно.
Стандартные чеки:
- Файл создан и не пустой
- Frontmatter содержит нужные поля
- В теле есть ключевые маркеры
- Daily note содержит запись о выполнении
- Связанные артефакты (cron job, форма, etc.) появились
4. Напиши harness в hermes-agent/docs/<skill>-iterate.sh
Минимальный шаблон:
#!/bin/bash
set -e
ITER="${1:-1}"
OUT_DIR="/tmp/<skill>-iterations"
API_URL="http://localhost:8642"
API_KEY="local-dev-key"
mkdir -p "$OUT_DIR"
echo "[1/4] Cleaning vault..."
docker exec hermes-dev sh -c "rm -rf /opt/data/secondbrain /opt/data/config.yaml /opt/data/SOUL.md" 2>/dev/null || true
docker restart hermes-dev > /dev/null
sleep 8
for i in {1..20}; do
curl -s "$API_URL/health" 2>/dev/null | grep -q "ok" && break
sleep 2
done
PROMPT_FILE="$OUT_DIR/prompt-$ITER.json"
cat > "$PROMPT_FILE" <<'PROMPT'
{
"model": "hermes-agent",
"input": "<сценарий промпт>",
"conversation": "<skill>-iter",
"store": true
}
PROMPT
echo "[2/4] Sending prompt..."
curl -s --max-time 300 "$API_URL/v1/responses" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d @"$PROMPT_FILE" > "$OUT_DIR/response-$ITER.json"
echo "[3/4] Collecting artefacts..."
docker exec hermes-dev cat /opt/data/secondbrain/<expected_file> 2>/dev/null > "$OUT_DIR/file-$ITER" || echo "" > "$OUT_DIR/file-$ITER"
echo "[4/4] Running checks..."
F=0
pass(){ echo " PASS $1"; }
fail(){ echo " FAIL $1"; F=$((F+1)); }
# Helper: extract text from response (decodes \uXXXX)
extract_text() {
python3 -c "
import json,sys
d=json.load(open(sys.argv[1]))
print('\n'.join([c.get('text','') for item in d.get('output',[]) if item.get('type')=='message' for c in item.get('content',[]) if c.get('type')=='output_text']))
" "$1"
}
T=$(extract_text "$OUT_DIR/response-$ITER.json")
# Your checks here:
[ -s "$OUT_DIR/file-$ITER" ] && pass "file_exists" || fail "file_exists"
# ...
echo ""
echo "Summary: $F FAILS / N checks"
5. Запусти первую итерацию
chmod +x docs/<skill>-iterate.sh
./docs/<skill>-iterate.sh 1
Смотри вывод. Если 0 FAIL — иди к шагу 8 (отчёт). Иначе шаг 6.
6. Анализируй каждый FAIL
Для каждого:
- Открой артефакт глазами — что в нём есть, чего не хватает?
- Прочитай ответ агента (из
response-N.jsonчерезextract_text) - Определи причину:
- Bug в инструкции — агент честно следовал, но инструкция не покрывает кейс
- Bug в харнесе — чек слишком жёсткий / regex не работает на русском / проверяет не там
- Нехватка предусловия — нужен env var, ключ в vault, MCP не подключён
7. Правь ОДНО и повторяй
- Инструкция — добавь шаг, переформулируй структурно (см. insights/instruction_composition)
- Харнес — расслабь regex, добавь decode JSON, проверь правильный путь к файлу
- Среда — добавь setup-шаг в начало харнеса
Каждое изменение → следующая итерация. Максимум 5 итераций. Если после 5 всё ещё FAIL — кейс сложный, эскалируй человеку с конкретным списком что пробовал.
8. Напиши отчёт в tests/<skill>.md
Секции должны идти в этом порядке — это важно для аудита и поиска по отчётам. Не меняй порядок и не добавляй промежуточные секции.
---
free: true
depends_on: ["[[instructions/<skill>]]"]
---
# Тест: <skill>
**Дата:** YYYY-MM-DD
**Harness:** `hermes-agent/docs/<skill>-iterate.sh`
**Модель:** gpt-5.5
**Метрика:** N FAIL / M чеков
## Сценарий
<один абзац>
## Чек-лист (M)
<пронумерованный список — точно столько пунктов сколько в Метрика>
## Путь итераций
**Обязательная таблица:**
| # | FAIL / N | Главный сдвиг |
|---|---|---|
| 1 | 4/10 | базовый скил |
| 2 | 3/10 | + ссылки на школу |
| ... | ... | ... |
| N | 0/10 | финал |
Под таблицей — короткий абзац на каждую итерацию (что упало конкретно, что меняли).
## Главный вывод
<один-два абзаца обобщения. Обязательно — что нашёл нового что не было в test_skill.md>
## Что меняли в инструкции
<или "ничего" если только харнес>
## Что меняли в харнесе
<или "ничего" если только инструкция>
## Артефакты
`/tmp/<skill>-iterations/` (или эквивалент)
## Открытые вопросы
<что не покрыто, что для будущего>
Если обновляешь существующий отчёт (например, после второго запуска) — замени старые секции, а не добавляй новые. Дублирующиеся «Что меняли в инструкции» через 10 строк друг от друга путают читателя.
9. Обнови tests/_index.md
Добавь строку в раздел "Отчёты":
- [[tests/<skill>]] — N итераций, путь X → 0 FAIL, что нашли
Что НЕ делать
- ❌ Не запускай тест без чистого vault — старый state может маскировать дефекты
- ❌ Не пиши тест-сценарий с подсказкой («используй CLI X, или прямой HTTP») — это даёт агенту лёгкий путь и тест теряет смысл
- ❌ Не делай чек на «правильность» по смыслу — только то что можно автоматизировать
- ❌ Не правь сразу инструкцию И харнес — потеряешь сигнал что починило
- ❌ Не клади креденшалы в харнес-скрипт открытым текстом — выноси в
/tmp/<test>.envсchmod 600
Стандартные ловушки
| Симптом | Причина | Фикс |
|---|---|---|
| Чек на русский слова падает | JSON эскейпит \uXXXX |
extract_text() через python decode |
[ -s file ] всегда false |
docker mounts ownership UID 10000 | docker exec ... cat вместо хостового read |
| API ключ в env не работает | Контейнер запущен без env var | Перезапусти контейнер с -e KEY=VALUE |
| MCP не подхватывает изменения | Сессия закэширована | Перезапуск контейнера или /reload-skills |
hermes cron list пусто |
CLI не сразу читает | Парсь /opt/data/cron/jobs.json напрямую |
Чек на конкретное слово (мало) падает на синониме (редко) |
Regex слишком узкий | Расширь alternation или сделай семантическую проверку через LLM-grader |
| Агент не записывает результат когда verification fail | Инструкция требует запись только на success | Добавь промежуточный state в инструкцию: pending_verification или аналог |
| В отчёте две секции «Что меняли в инструкции» подряд | Старый черновик не заменён при втором проходе | Когда обновляешь отчёт — замени секции, не приписывай новые в конец |
Параллелизм
Можно тестировать несколько скилов параллельно если нет общего state:
- Каждый тест в свой
/tmp/<skill>-iterations/ - Используй разные
conversationID в промпте - НО: если оба чистят vault — конфликт, гоняй последовательно
Для разных hermes-инстансов (план plans/e2e-public-hermes в hermes-agent) — каждый тест на свой инстанс, полная независимость.
Pre-flight check для исполнителя
Перед написанием харнеса убедись что в твоей среде есть права на bash:
bash --version 2>&1 | head -1 && docker ps > /dev/null && echo "ok"
Если эту команду нельзя выполнить — ты в среде планировщика, не исполнителя. Тогда твоя роль:
- Написать harness и положить его в
hermes-agent/docs/<skill>-iterate.sh - Написать черновик отчёта
tests/<skill>.mdс пометкой «harness готов, запуск ожидается» - Передать управление обратно — другая роль с правами bash запустит и доведёт
Загрузка кредов
Если тест требует креденшалы (cookie, API key, token):
# В харнесе:
ENV_FILE="/tmp/<test_name>.env"
[ -f "$ENV_FILE" ] && set -a && source "$ENV_FILE" && set +a
Файл создаётся вне харнеса (вручную или другим скриптом), с правами 600. В харнесе только подгружается.
Разделение ролей
| Роль | Что делает | Что не делает |
|---|---|---|
| Планировщик | пишет harness, выбирает чеки, пишет черновик отчёта | не запускает bash |
| Исполнитель | запускает harness, собирает артефакты, обновляет отчёт реальными числами | не пишет с нуля — следует тому что подготовил планировщик |
| Аналитик | анализирует FAIL'ы, правит инструкцию или харнес | один раз меняет одно |
Один человек/агент может играть все роли. Но при делегации через Agent({subagent_type:...}) каждая роль может стать отдельной задачей.
Протокол передачи между ролями
Планировщик → Исполнитель:
- Harness лежит в
hermes-agent/docs/<skill>-iterate.sh(исполняемый). - Креды (если нужны) — в
/tmp/<skill>-test.env(mode 600), не в самом harness'е. - Черновик отчёта в
tests/<skill>.mdс строкой**Метрика:** harness готов, прогон ожидается— это маркер что нужно сделать. - Все ожидаемые входы среды (контейнер, API endpoint, env vars) — перечислены в секции «Предусловия» отчёта.
Исполнитель → Аналитик (если FAIL):
- Запустил harness, получил
Summary: K FAILS / N. - Заменяет строку Метрика на реальные числа. Не добавляет — заменяет.
- Если K > 0 — добавляет первую строку в обязательную таблицу «Путь итераций».
- Передаёт управление аналитику с конкретным списком: вот эти чеки упали, артефакты в
/tmp/....
Аналитик → Исполнитель (после правки):
- Меняет одно: либо инструкцию, либо harness.
- Объясняет в одну строку почему правит — будет в графе «Главный сдвиг» новой строки таблицы.
- Передаёт обратно для следующей итерации.
Связанные
- insights/instruction_composition — почему структурные правки сильнее текстовых
- ru/thoughts/testing-agent-skills (на сайте trip2g) — общий подход и история подхода
- Скилы в hermes-agent: docs/landing-iterate.sh, docs/persona-iterate.sh, docs/timezone-iterate.sh и т.д. — реальные примеры harness'ов
Кейс — успешный путь
См. tests/request_admin_rights: 5 итераций, 3 → 1 → 0 FAIL. Каждая итерация — одна правка либо инструкции либо харнеса. Финальный 0 FAIL — после правки инструкции (добавили pending_verification state) и харнеса (декодировали JSON).
Расширенный режим (когда хочется измерять, а не только "работает")
Базовый цикл выше отвечает на вопрос «работает ли скил». Расширенный — отвечает на «насколько лучше со скилом чем без» и «не деградирует ли при правках». Подсмотрено в публичном framework'е для оценки скилов от Anthropic.
Структура workspace
Вместо одной папки /tmp/<skill>-iterations/ — дерево:
tests-workspace/<skill>/
├── iteration-1/
│ ├── eval-0-<name>/
│ │ ├── with_skill/ ← результаты прогона СО скилом
│ │ │ ├── prompt.json
│ │ │ ├── response.json
│ │ │ └── artefacts/ ← файлы из vault
│ │ ├── without_skill/ ← БАЗЛАЙН без скила
│ │ │ └── ...
│ │ ├── eval_metadata.json ← prompt, assertions, file inputs
│ │ ├── timing.json ← токены, длительность
│ │ └── grading.json ← pass/fail с evidence по каждой assertion
│ ├── eval-1-<name>/
│ └── benchmark.json ← агрегат: pass_rate, avg_tokens, delta
├── iteration-2/
└── ...
Каждая итерация — полный снимок. Можно вернуться к iteration-1 и сравнить с iteration-5.
Что добавить к шагу 2 (сценарии)
Вместо одного промпта — 2–3 реалистичных. Каждый — отдельный eval-N. Параллельный прогон.
Что добавить к шагу 3 (чеки) — assertions
Каждый чек — формальная assertion с описанием и типом:
{
"id": "user_settings_has_timezone",
"description": "файл user_settings.md содержит поле timezone:",
"type": "objective"
}
Тип objective — проверяемое программно. Тип subjective — оценка качества (например, тон ответа), не для всех скилов нужен.
Baseline (новый шаг 5а)
Параллельно с прогоном СО скилом — прогоняй БЕЗ. Это контрольная группа.
- Без скила — агент не получает доступ к школьной инструкции (или её нет в школе ещё). Видишь что он сделает «своими силами».
- Со старой версией — для improve-кейса. Снапшот старого
<skill>.mdзапускается на тех же сценариях.
Метрика — pass_rate_delta = pass_rate_with - pass_rate_without. Если delta = 0 или меньше — скил не даёт пользы. Серьёзный сигнал.
Timing & tokens (новый шаг 5b)
Hermes API в /v1/responses возвращает usage. Сохрани в timing.json:
{
"input_tokens": 1234,
"output_tokens": 567,
"total_tokens": 1801,
"duration_seconds": 15.2
}
Полезно для:
- Поймать когда скил раздувает контекст (input_tokens сильно вырос — инструкция большая)
- Поймать когда скил экономит токены (агент находит ответ быстрее)
Aggregation (новый шаг 7а — между прогонами и отчётом)
После всех eval-K прогонов соберите benchmark.json:
{
"iteration": 1,
"configurations": [
{
"name": "with_skill",
"pass_rate": 0.83,
"avg_tokens": 12500,
"avg_duration_seconds": 18.4
},
{
"name": "without_skill",
"pass_rate": 0.33,
"avg_tokens": 8200,
"avg_duration_seconds": 11.0
}
],
"comparison": {
"pass_rate_delta": 0.50,
"token_delta": 4300
}
}
Прочесть глазами или питон-скриптом — видно что скил повышает успех на 50% но стоит 4300 лишних токенов. Это компромисс — пусть пользователь решает.
Grader как отдельный шаг
Вместо bash-grep в харнесе — отдельный шаг «grade»:
- Все артефакты собраны в
iteration-N/eval-K/with_skill/ - Запускаешь grader — это другой prompt к Hermes API (или к Claude напрямую):
- input: артефакты + assertions
- output:
grading.jsonс pass/fail и evidence по каждой
- Программируемые assertions (regex, exit code) — выполняй скриптом
- Субъективные (если есть) — другой агент-grader делает суждение
Зачем разнесение: пишущий harness и оценивающий — разные роли. Один регекс может работать на русском JSON с unicode и не работать на ASCII выводе.
Trigger eval — для тюнинга description (опционально, отдельная задача)
Если скил не подбирается агентом сам (не срабатывает на нужные промпты или срабатывает на лишние) — отдельный набор:
[
{"query": "проверь форму на новые заявки", "should_trigger": "triage_form_submits"},
{"query": "сделай мне кофе", "should_trigger": null}
]
Прогон: для каждого запроса смотришь какой скил агент берёт. Метрика — precision/recall.
Это отдельный цикл, не часть тестирования поведения. Делай только когда базовый тест 0 FAIL и хочется ещё дотюнить.
Что НЕ переносим
- HTML viewer с
feedback.json— у нас текстовый flow - Автоматический run_loop для description optimization — наш скил подбирается через школьный MCP, эффективнее править вручную
- Packaging — все скилы уже в школе
Когда базовый режим, когда расширенный
| Скил | Режим |
|---|---|
| Простая инструкция с 1 артефактом (setup_timezone, create_persona) | базовый |
| Композиция нескольких инструкций (setup_idle_check_in) | базовый + одна базлайн прогон желателен |
| Сложный e2e со внешним сервисом (request_admin_rights, triage_form_submits) | расширенный — baseline нужен для понимания delta |
| Скил под публичные игры (3 hermes-инстанса) | расширенный + trigger eval — для воспроизводимости |