Инструкция: Протестировать скил автономным циклом

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

Для каждого:

  1. Открой артефакт глазами — что в нём есть, чего не хватает?
  2. Прочитай ответ агента (из response-N.json через extract_text)
  3. Определи причину:
    • 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/
  • Используй разные conversation ID в промпте
  • НО: если оба чистят vault — конфликт, гоняй последовательно

Для разных hermes-инстансов (план plans/e2e-public-hermes в hermes-agent) — каждый тест на свой инстанс, полная независимость.

Pre-flight check для исполнителя

Перед написанием харнеса убедись что в твоей среде есть права на bash:

bash --version 2>&1 | head -1 && docker ps > /dev/null && echo "ok"

Если эту команду нельзя выполнить — ты в среде планировщика, не исполнителя. Тогда твоя роль:

  1. Написать harness и положить его в hermes-agent/docs/<skill>-iterate.sh
  2. Написать черновик отчёта tests/<skill>.md с пометкой «harness готов, запуск ожидается»
  3. Передать управление обратно — другая роль с правами 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:...}) каждая роль может стать отдельной задачей.

Протокол передачи между ролями

Планировщик → Исполнитель:

  1. Harness лежит в hermes-agent/docs/<skill>-iterate.sh (исполняемый).
  2. Креды (если нужны) — в /tmp/<skill>-test.env (mode 600), не в самом harness'е.
  3. Черновик отчёта в tests/<skill>.md с строкой **Метрика:** harness готов, прогон ожидается — это маркер что нужно сделать.
  4. Все ожидаемые входы среды (контейнер, API endpoint, env vars) — перечислены в секции «Предусловия» отчёта.

Исполнитель → Аналитик (если FAIL):

  1. Запустил harness, получил Summary: K FAILS / N.
  2. Заменяет строку Метрика на реальные числа. Не добавляет — заменяет.
  3. Если K > 0 — добавляет первую строку в обязательную таблицу «Путь итераций».
  4. Передаёт управление аналитику с конкретным списком: вот эти чеки упали, артефакты в /tmp/....

Аналитик → Исполнитель (после правки):

  1. Меняет одно: либо инструкцию, либо harness.
  2. Объясняет в одну строку почему правит — будет в графе «Главный сдвиг» новой строки таблицы.
  3. Передаёт обратно для следующей итерации.

Связанные

  • 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»:

  1. Все артефакты собраны в iteration-N/eval-K/with_skill/
  2. Запускаешь grader — это другой prompt к Hermes API (или к Claude напрямую):
    • input: артефакты + assertions
    • output: grading.json с pass/fail и evidence по каждой
  3. Программируемые assertions (regex, exit code) — выполняй скриптом
  4. Субъективные (если есть) — другой агент-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 — для воспроизводимости