Синтетические сообщения об ошибке API (model <synthetic>) не считаются ходом модели (O-18, живой прогон). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
20 KiB
Design: /harness-retro
Context
Ретро 11.10.2026 (docs/retro/2026-10-11.md) сделано вручную. Таблицу «Цифры» собрали из вывода retro_stats.py и retro_blocks.py, находки — по прочтению 47 файлов памяти. В скриптах зашиты имена репозиториев awp-ca, вывод — текст, тестов нет. Подсчёт блокировок завышен: в 103 вошли выводы команд со словом BLOCKED, настоящих ошибок хука было 76 (O-1, #6).
После ретро вышли хуки по цели записи (0.4.0, корпус 129 записей), роли (0.3.0), allow-правила (0.5.0) и worktree в .claude/worktrees/ (0.5.1). Что из этого помогло, видно только по сравнимым цифрам.
Владелец решил (brainstorming 24.10.2026):
- результат — черновик ретро-документа;
- срез — один проект или workspace;
- запуск — из репозитория обвязки, проект передаётся аргументом;
- ложные блокировки размечает агент по спеке, и они идут в корпус;
- хвосты закрытия ищутся по текущему состоянию репозиториев;
- поправки владельца по процессу собираются в ретро;
- подход A — проектный skill и модуль метрик с тестами.
Транскрипты Claude Code: ~/.claude/projects/<slug>/<session>.jsonl, по строке на запись. Типы записей: user, assistant (message.content: text, tool_use; message.usage), system (subtype: compact_boundary), ai-title, permission-mode. У записей есть cwd, gitBranch, timestamp.
Сессия, перешедшая в worktree, пишет транскрипт в каталог со slug пути worktree. Пример — эта сессия: -Users-andrejepifancev-Projects-cc-harness-wt-6-guard-main-target.
Goals / Non-Goals
Goals
- Повторяемый срез метрик одного проекта в JSON и сравнение с прошлым ретро.
- Блокировки среза размечаются и становятся регрессионными записями корпуса; известные ошибки хуков видны в doctor.
- Кандидаты в поправки владельца с контекстом; агент отбирает и группирует.
- Хвосты чеклиста закрытия по состоянию репозиториев.
- Черновик ретро по шаблону, утверждение через PR.
Non-Goals
- Сторона Gitea в хвостах: открытые issue после слитого PR, пропущенные follow-up.
- Срез по нескольким проектам сразу.
- Автоматические находки по порогам.
- Автоматическая очистка хвостов: скрипт только подсказывает команду.
- Переписывать ретро 11.10.
Decisions
1. Проектный skill, без установки
/harness-retro нужен только в сессии в репозитории обвязки: туда пишется черновик, там корпус и hook_cases_build.py. Поэтому skill проектный (.claude/skills/harness-retro/), как skills openspec. install.sh и ~/.claude не меняются.
.claude/skills/ не входит в .claude/code-paths, поэтому SKILL.md и шаблон — документация. Код лежит в docs/retro/scripts/, как и сейчас.
2. retro_metrics.py: модуль на функциях
Только stdlib, Python 3.8+. CLI:
retro_metrics.py <папка проекта> [--since ГГГГ-ММ-ДД] [--harness <репозиторий обвязки>]
[--projects-root <каталог>] [--out <metrics.json>]
[--markdown [--prev <прошлый.metrics.json>]] [--corrections]
--projects-rootпо умолчанию —${CLAUDE_CONFIG_DIR:-~/.claude}/projects; тесты подставляют свой.--harnessпо умолчанию — репозиторий, в котором лежит скрипт. Из него берутся даты прошлых ретро для--since.
Функции:
slug(path);project_sessions(project, root, since)— список транскриптов среза;read_session(path)— генератор записей, битые строки считаются;session_stats(records, code_of)— словарь счётчиков сессии;aggregate(stats)— метрики среза;corrections(records)— кандидаты;to_markdown(metrics, prev);main(argv).
Каждая метрика считается в одном месте, в session_stats, и проверяется отдельным тестом.
3. Сессии проекта
Каталоги-кандидаты — те, чьё имя начинается со slug проекта. В срез сессия входит по первому cwd: путь внутри проекта или внутри <проект>.wt/ (старое место worktree). Префикс slug нужен, чтобы найти каталоги worktree; проверка cwd отсекает чужой проект /p/repo-tools при проекте /p/repo.
4. Что такое «код»
Пути в транскриптах относятся к репозиториям, которых может уже не быть: worktree удалены. Относительный путь считается от корня worktree или репозитория. Корень определяется в таком порядке:
- часть пути после
/.claude/worktrees/<имя>/; - затем после
<репо>.wt/<имя>/; - затем после
<workspace>/.wt/<задача>/<репо>/; - затем после ближайшего существующего каталога с
.git.
Префиксы кода берутся из .claude/code-paths основного репозитория, без него — правило src/ на любой глубине. Так же решает require-plan.
Bash-команда считается чтением кода, если хотя бы один её простой сегмент (разбиение по &&, ||, ;, |) начинается с grep, rg, find, cat, sed -n или head и хотя бы один аргумент после разбора shlex (при ошибке — split) указывает в каталог кода относительно cwd.
5. Блокировки, отказы, роли
- Блокировка: результат инструмента совпадает с
^\s*PreToolUse:\S+ hook errorи содержитBLOCKED. Хук определяется так же, как вhook_blocks_export.hook_of, функция импортируется. - Отказ классификатора:
denied by the Claude Code auto mode classifier. Reason: \[([^\]]+)\]. - Отказ владельца: результат инструмента содержит
The user doesn't want to proceed with this tool use. Прерывание ([Request interrupted by user…) считается отдельной метрикой, а не отказом. - Ход модели — уникальный
message.idассистента. Claude Code пишет одно сообщение несколькими строками с общимidи одинаковымusage(проверено на транскриптах этой машины: 538 id на 1381 строку). Поэтому токены считаются один раз наid. Строки безidсчитаются по одной. - Сессия — файл
<id>.jsonl. Транскрипт переезжает в каталог worktree (записьrelocated); если один id встретился в нескольких каталогах, берётся самый большой файл. - Роль: первый
tool_useсname: Skillиinput.skill=role(или…:role), аргумент —input.args.
6. Кандидаты в поправки
Признаки ищутся в первых 200 символах промпта, без учёта регистра и с границами слов. Список признаков — в спеке. Промпт длиннее 2000 символов признаком не проверяется: это вставка материала.
Последнее действие — последний tool_use ассистента перед промптом: Bash: <команда>, Edit|Write|Read: <путь>, <Имя> для остальных. Хвост ответа — последние 200 символов последнего текстового блока ассистента.
Весь вывод проходит через redact() из hook_blocks_export.py.
7. retro_leftovers.py
Репозитории: сам git-проект или каталоги из таблицы workspace (разбор как в doctor.sh). Для каждого:
git branch -r --merged origin/mainбезorigin/mainиorigin/HEAD;git for-each-ref --format='%(refname:short) %(upstream:track)' refs/heads, где слитые вorigin/mainили[gone];git worktree list --porcelain— ветка из слитых илиprunable;ls .superpowers/sdd/*/в основном checkout и в каждом worktree;git ls-tree -d --name-only origin/main openspec/changes/безarchive;git rev-list --count HEAD..origin/mainиgit status --porcelainосновного checkout.
Нет origin/main — у репозитория одна строка kind: no-origin-main. --fetch перед проверками делает git fetch --prune origin.
8. Корпус: сегменты и known
Формат hook-cases.jsonl: строка с ключом layout — шапка сегмента {home, layout}, следующие строки — записи этого сегмента. Первый сегмент — пилот 12.10 и живые случаи, без изменений.
hook_corpus.py строит песочницу на каждый сегмент (<tmp>/seg-<n>/home). Поле known:
- расхождение при
known—KNOWN <id>, тест не падает; - совпадение при
known—KNOWN-FIXED <id>, тест падает.
Итог: корпус хуков: N/M, при известных ошибках — корпус хуков: N/M, известных ошибок K. N — записи без расхождений, считая известные; M — все записи.
hook-labels-<дата>.json:
{"home": "/Users/x", "layout": [...], "cases": {"2026-11-01-001": {"expected": "allow", "why": "devnull"},
"2026-11-01-002": {"skip": "ветка репозитория на момент вызова неизвестна"},
"2026-11-01-003": {"expected": "allow", "why": "read-only", "known": "RT-21"}}}
hook_cases_build.py падает, если у записи выгрузки нет разметки: ретро не уходит с неразмеченными блокировками.
9. metrics.json и сравнение
Схема {"schema": 1, "period": {"since", "until"}, "project", "sources": {...}, "metrics": {...}}. Ключи metrics стабильны: по ним --prev сопоставляет строки. Метрика, которая больше не считается, хранится как {"retired": "<причина>"}.
2026-10-11.metrics.json заполняется из таблицы «Цифры» 11.10. Блокировки — по разметке корпуса: 76 настоящих ошибок хуков, из них 4 по делу, ложных 72 (95 %). В таблице 11.10 было 103 — это завышено (O-1). cd в main — retired.
10. Шаблон и порядок skill
template.md повторяет разделы ретро 11.10 и добавляет «Поправки владельца» и «Что не измерялось». SKILL.md — шаги:
- issue и worktree;
retro_metrics.py --out;hook_blocks_export.py --since→ разметка →hook_cases_build.py→tests/run.sh;retro_leftovers.py --fetch;--corrections→ отбор и группировка;- чтение памяти целиком;
- черновик и
--markdown --prev; - коммит и PR.
Маркеры: находки RT-n по сквозной нумерации ретро.
Risks / Trade-offs
- Формат транскриптов Claude Code не документирован. Признаки (
hook error,compact_boundary, текст отказа) могут смениться. Смягчение: тесты на синтетических транскриптах фиксируют известный формат, а неожиданные записи пропускаются. Изменение формата увидят нулевые метрики при новом ретро. - Эвристика поправок шумит. «Почему» часто бывает вопросом по содержанию. Скрипт только предлагает кандидатов, решает агент, проверяет владелец в PR.
- Корень пути у удалённого worktree определяется по шаблону пути (решение 4). Нестандартный путь даёт правило
src/, и чтения кода могут не посчитаться. - Разметка блокировок требует раскладки песочницы, которую агент восстанавливает по транскриптам. Ошибка в раскладке даёт неверный вердикт в корпусе. Смягчение:
skipи ревью PR. KNOWN-FIXEDвалит тесты при исправлении хука, пока не снята пометка. Это сделано намеренно: исправление хука включает снятиеknown.
Migration Plan
- Добавить скрипты, тесты, skill и шаблон; изменить
hook_corpus.pyиhook_cases_build.py— корпус пересобирается без изменения записей. 2026-10-11.metrics.json— из таблицы ретро 11.10.- Удалить
retro_stats.pyиretro_blocks.py, поправить ссылки вCLAUDE.md. - После merge —
bash install.sh --no-permissions: doctor и раннер корпуса. Первое ретро — по команде владельца на машине с полигоном.
Откат — revert PR. Корпус возвращается к одному сегменту.
Decisions made without discussion
| Дата | Решение | Обоснование | Цена ошибки |
|---|---|---|---|
| 2026-10-24 | Skill проектный, install.sh не меняется |
Запуск решён «из репозитория обвязки»; данные корпуса и черновик там же | Чтобы запускать из другого каталога, придётся ставить skill глобально |
| 2026-10-24 | Сессия входит в срез по cwd первой записи, а каталоги ищутся по префиксу slug |
Префикс находит каталоги worktree, cwd отсекает чужой проект с тем же префиксом |
Сессия, начатая вне проекта и перешедшая в него, не войдёт в срез |
| 2026-10-24 | Корень пути удалённого worktree — по шаблонам .claude/worktrees/, .wt/ |
Каталогов уже нет, а code-paths нужен для «кода» |
Нестандартное место worktree: чтения кода не посчитаются |
| 2026-10-24 | Признаки поправок — в первых 200 символах; промпты > 2000 символов не проверяются | Длинные промпты — вставки материала, признаки в них случайны | Пропуск поправки внутри длинного сообщения |
| 2026-10-24 | KNOWN-FIXED — FAIL |
Иначе пометка known переживёт исправление и скроет регрессию |
Исправление хука требует ещё одной правки (снять known) |
| 2026-10-24 | hook_cases_build.py падает на записи без разметки |
Ретро не должно уходить с неразмеченными блокировками | Нельзя закоммитить частичную разметку |
| 2026-10-24 | Метрика cd в main — retired |
RT-2 снят хуками 0.4.0; счёт был по именам репозиториев awp-ca | Потеря одного ряда сравнения |
| 2026-10-24 | Блокировки 11.10 в metrics.json — 76 ошибок хука, 72 ложных (по корпусу), а не 103 из таблицы |
103 завышено выводами со словом BLOCKED (O-1) |
Сравнение с таблицей 11.10 по этой строке расходится с документом |
| 2026-10-24 | retro_stats.py и retro_blocks.py удаляются |
Их заменяет retro_metrics.py; в ретро 11.10 они упомянуты как история |
Чтобы воспроизвести 11.10 буква в букву, нужен git history |
| 2026-10-24 | Ходы и токены — по уникальному message.id, а не по строкам транскрипта |
Строки одного сообщения повторяют usage; подсчёт по строкам завышает токены в 2–3 раза |
Ходы и токены 11.10 (по строкам) несравнимы; в 2026-10-11.metrics.json они помечены |
| 2026-10-24 | Записи ассистента с isApiErrorMessage или model: <synthetic> — не ход модели: не считаются в ходах, токенах и стартовом контексте |
Это сообщения Claude Code об ошибке API с нулевым usage; на живых транскриптах давали стартовый контекст 0 (O-18) |
Если Claude Code пометит так настоящий ход, он выпадет из подсчёта |
| 2026-10-24 | Свежая ветка, совпадающая с origin/main, — не хвост |
--merged считает слитой и ветку без коммитов, например только что созданный worktree |
Брошенная пустая ветка не попадёт в хвосты |
| 2026-10-24 | Сторона Gitea в хвостах не проверяется | Хрупко (формат описаний PR и issue); агент видит это при чтении | Незакрытая issue после merge не попадёт в метрику |