- C-1: транскрипты сабагентов (<сессия>/subagents/*.jsonl) вливаются в сессию-родителя — ходы, токены, инструменты, codegraph, блокировки, отказы; промпты, роль, стартовый контекст — только из основного. hook_blocks_export.py выгружает их блокировки с сессией-родителем. - I-7: период режется по меткам времени записей; сессия на стыке делится между ретро, стартовый контекст — если первый ход в периоде. - I-1: простые команды Bash — shlex с punctuation_chars, «|» в кавычках команду не делит. I-2: правило src/ ловит и сам каталог src. - I-3: clip() — redact до обрезки (кандидаты, выгрузка), роль — redact. - I-4: без --since период берётся из существующего --out; --from рисует «Цифры» из сохранённого среза. SKILL.md передаёт период явно. - I-5: выгрузка ретро без разметки — сборка корпуса падает. - I-6: сводка компакции — не промпт; «перечитай» — побуждение перечитать. - 2026-10-11.metrics.json: несравнимое помечено (без сабагентов, широкий «перечитай»). Спека и design.md (Decisions) обновлены. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
25 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.
Период режется по записям, а не по сессиям: запись входит, если её метка времени (у записи без метки — предыдущая) не раньше since. Сессия входит, если в ней есть ходы или промпты периода. Стартовый контекст — только если первый ход сессии в периоде. Так длинная сессия на стыке периодов (владелец держит одну сессию на много задач) делится между ретро, а не теряется.
Транскрипты сабагентов <каталог>/<сессия>/subagents/*.jsonl (Claude Code пишет их отдельно, в основном транскрипте isSidechain нет) вливаются в сессию-родителя: ходы, токены, инструменты, codegraph, блокировки, отказы. Промпты, роль, стартовый контекст и кандидаты в поправки — только из основного транскрипта.
4. Что такое «код»
Пути в транскриптах относятся к репозиториям, которых может уже не быть: worktree удалены. Относительный путь считается от корня worktree или репозитория. Корень определяется в таком порядке:
- часть пути после
/.claude/worktrees/<имя>/; - затем после
<репо>.wt/<имя>/; - затем после
<workspace>/.wt/<задача>/<репо>/; - затем после ближайшего существующего каталога с
.git.
Префиксы кода берутся из .claude/code-paths основного репозитория, без него — правило src/ на любой глубине. Так же решает require-plan.
Bash-команда считается чтением кода, если хотя бы одна её простая команда начинается с grep, rg, find, cat, sed -n или head и хотя бы один аргумент указывает в каталог кода относительно cwd. Простые команды выделяет shlex с punctuation_chars: кавычки учитываются, делят &&, ||, ;, |, & и скобки, цель редиректа пропускается; при незакрытой кавычке — split. Правило src/ срабатывает и на сам каталог src в аргументе.
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, которые сами хвост (слитая илиgoneветка,prunable). SDD workspace в 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 | SDD workspace в worktree неслитой ветки — не хвост | Живой прогон показал рабочий каталог текущего плана как хвост (O-19); это работа в процессе | Брошенная неслитая задача со своим SDD workspace в хвосты не попадёт вовсе: worktree на неслитой ветке скрипт тоже не выводит |
| 2026-10-24 | Транскрипты сабагентов вливаются в сессию-родителя (C-1, финальное ревью) | В режиме SDD почти вся работа — в сабагентах; без них замер codegraph (решение владельца 3) пуст по построению | Метрики 11.10 считались без сабагентов: tool_calls, hook_blocks, classifier_denials, codegraph.init/explore в 2026-10-11.metrics.json помечены несравнимыми |
| 2026-10-24 | Период режется по меткам времени записей, а не по первой метке сессии (I-7, финальное ревью) | Владелец держит одну сессию на много задач; при фильтре по началу сессии её работа после прошлого ретро не измерялась бы никогда | Сессия на стыке делится между двумя ретро; запись без метки относится к предыдущей |
| 2026-10-24 | Без --since период берётся из существующего --out; «Цифры» — --from сохранённого среза (I-4, финальное ревью) |
Черновик docs/retro/<D>.md иначе сдвигал период по умолчанию на дату самого ретро |
Чтобы пересчитать срез за другой период, нужен явный --since или новый --out |
| 2026-10-24 | Простые команды Bash — shlex с punctuation_chars, а не Lexer хука (I-1, финальное ревью) |
Самодостаточно, учитывает кавычки; ретро не зависит от внутреннего API harness_target.py |
Разбор heredoc и $(…) грубее, чем у хука: чтение кода внутри них может не посчитаться |
| 2026-10-24 | Секреты заменяются до обрезки — clip() в hook_blocks_export.py (I-3, финальное ревью) |
Обрезка до redact оставляла обрывок токена, который шаблон уже не узнаёт |
Нет |
| 2026-10-24 | «Перечитай» — побуждение перечитать рядом с «память», «контекст», .md; сводка компакции — не промпт (I-6, финальное ревью) |
Широкий регэксп 11.10 ловил любое упоминание контекста и сводки компакции | recall 11.10 (широкий регэксп) помечен несравнимым |
| 2026-10-24 | Свежая ветка, совпадающая с origin/main, — не хвост |
--merged считает слитой и ветку без коммитов, например только что созданный worktree |
Брошенная пустая ветка не попадёт в хвосты |
| 2026-10-24 | Сторона Gitea в хвостах не проверяется | Хрупко (формат описаний PR и issue); агент видит это при чтении | Незакрытая issue после merge не попадёт в метрику |