feat(retro): skill /harness-retro, шаблон ретро, метрики 11.10; документация 0.6.0 (#29)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
andrey.epifantsev 2026-10-24 01:28:56 +04:00
parent e5c00a5442
commit cc697c030c
10 changed files with 198 additions and 303 deletions

View File

@ -0,0 +1,38 @@
---
name: harness-retro
description: Ретро обвязки claude-harness по транскриптам, памяти и репозиториям одного проекта или workspace — срез метрик, разметка блокировок хуков в корпус, хвосты закрытия, поправки владельца, черновик docs/retro/<дата>.md и PR. Запускать в сессии в репозитории обвязки по команде «/harness-retro <папка> [--since ГГГГ-ММ-ДД]» после среза.
---
# /harness-retro
Аргумент — папка проекта или workspace, который изучаем. Сессия — в репозитории обвязки, роль dev. Дата ретро `<D>` — сегодня, `ГГГГ-ММ-ДД`. Скрипты — `docs/retro/scripts/`, все команды — из корня worktree ретро. Спека — `openspec/specs/harness-retro/spec.md`.
## Шаги
1. **Задача и worktree.** Issue «Ретро <D>: <проект>» (`tea issues create`). Затем `EnterWorktree` с именем `retro-<D>`, ветка `git branch -m retro/<D>`, `codegraph init` в worktree.
2. **Срез.** Выполнить `python3 docs/retro/scripts/retro_metrics.py <папка> [--since …] --out docs/retro/<D>.metrics.json`.
- Exit 1 — сессий нет. Сообщить владельцу slug и период и остановиться.
- Битых строк больше 1 % — отметить в «Что не измерялось».
3. **Блокировки.** Сначала выгрузить: `python3 docs/retro/scripts/hook_blocks_export.py --since <since> --id-prefix <D> <каталоги из sources.dirs> > docs/retro/fixtures/hook-blocks-<D>.jsonl`. Каталоги — `~/.claude/projects/<имя>`.
- Разметить каждую запись в `docs/retro/fixtures/hook-labels-<D>.json`:
- `expected` (`allow|block`) и `why` по спеке `hook-guards`;
- раскладку песочницы — по `gitBranch` сессий и выводу `git worktree list` и `git branch` в транскриптах;
- если не восстановить — `skip` с причиной.
- Собрать корпус: `python3 docs/retro/scripts/hook_cases_build.py > skills/harness-doctor/fixtures/hook-cases.jsonl` и прогнать `bash tests/run.sh hook-corpus`. Если текущий хук даёт не тот вердикт — проставить записи `"known": "RT-n"` и завести находку.
- Пересчитать срез с `--labels docs/retro/fixtures/hook-labels-<D>.json --out docs/retro/<D>.metrics.json`.
4. **Хвосты закрытия.** `python3 docs/retro/scripts/retro_leftovers.py <папка> --fetch --markdown`. Ничего не удалять: таблица идёт в черновик, уборка — по решению владельца.
5. **Поправки владельца.** `python3 docs/retro/scripts/retro_metrics.py <папка> [--since …] --corrections`.
- Отобрать только поправки по процессу: пропущенный шаг, лишний вопрос, неверный совет, обход правила, расширение задачи. Вопросы и просьбы по содержанию выбросить.
- Сгруппировать по видам.
- Вид, который повторяется (2 раза и больше) или стоил владельцу заметного времени, — находка `RT-n` и пункт бэклога.
6. **Качественная часть.** Прочитать память проекта целиком (`~/.claude/projects/<slug>/memory/`) и примеры из выгрузок. Находки `RT-n` — сквозная нумерация: максимум в прошлых `docs/retro/*.md` плюс 1. Приоритет: P1 — теряет время каждую неделю или ломает процесс; P2 — трение; P3 — наблюдение.
7. **Черновик.** Скопировать `.claude/skills/harness-retro/template.md` в `docs/retro/<D>.md` и заполнить.
- «Цифры»: `python3 docs/retro/scripts/retro_metrics.py <папка> [--since …] --labels … --markdown --prev docs/retro/<прошлое>.metrics.json`.
- Секретов в черновике быть не должно. Цитаты брать только из вывода скриптов: он уже прошёл `redact`.
8. **PR.** `bash tests/run.sh` должен быть зелёным. Коммит: черновик, `metrics.json`, выгрузка, разметка, корпус. Затем `git push -u origin retro/<D>` и `tea pulls create` со ссылкой на issue. Merge — только владелец.
## Не делать
- Не удалять хвосты и не чинить хуки в PR ретро — это находки и бэклог.
- Не заполнять раздел «Решения владельца»: он пишется на ревью PR.
- Не вставлять в черновик сырые куски транскриптов мимо скриптов.

View File

@ -0,0 +1,49 @@
# Ретроспектива обвязки claude-harness — <ДД.ММ.ГГГГ>
Период: <since>–<until>, полигон — <проект или workspace>.
Предыдущая точка сравнения — `docs/retro/<прошлое>.md`.
Источники:
- <N> транскриптов сессий (<каталоги из sources.dirs>, <объём>);
- <N> файлов auto-memory (<объём>);
- состояние репозиториев (`retro_leftovers.py`);
- блокировки среза, размеченные в корпус: `docs/retro/fixtures/hook-labels-<D>.json`.
Количественную часть считал `docs/retro/scripts/retro_metrics.py`, качественную — по прочтению памяти целиком и кандидатов в поправки.
Маркировка находок: `RT-n`, продолжение нумерации прошлых ретро. Приоритет: P1 — теряет время каждую неделю или ломает процесс; P2 — трение; P3 — наблюдение.
## 1. Цифры
<таблица из retro_metrics.py --markdown --prev>
Хвосты чеклиста закрытия:
<таблица из retro_leftovers.py --markdown>
## 2. Находки
**RT-<n> (P<k>). <Одна фраза — что происходит>.** <Факты: где, сколько раз, чем стоило>. Источники: <сессии, память, метрика>.
## 3. Поправки владельца
| Вид | Сколько | Пример (цитата, что было до неё) | Что вышло | Находка |
|---|---|---|---|---|
## 4. Что сработало (сохранить)
- <…>
## 5. Бэклог улучшений (по приоритету)
| # | Что | Находки | Оценка |
|---|---|---|---|
## 6. Решения владельца (<дата>)
Заполняется на ревью PR.
## 7. Что не измерялось
- Сторона Gitea чеклиста закрытия: открытые issue после слитого PR, пропущенные follow-up.
- <…>

View File

@ -1,5 +1,19 @@
# Changelog # Changelog
## 0.6.0 — 2026-10-24
- **`/harness-retro`** (B12, #29) — проектный skill регулярного ретро обвязки. Работает по циклу фичи: issue, worktree, PR. Результат — черновик `docs/retro/<дата>.md` по шаблону и `<дата>.metrics.json` со сравнением с прошлым ретро.
- `docs/retro/scripts/retro_metrics.py` считает срез метрик проекта по транскриптам и памяти:
- сессии, промпты, инструменты, роли, сабагенты, skills, токены (по `message.id`, без синтетических сообщений об ошибке API), компакции, стартовый контекст;
- блокировки хуков, отказы классификатора и владельца;
- codegraph против чтения кода (решение владельца 3);
- «перечитай», память;
- кандидаты в поправки владельца по процессу (`--corrections`).
Заменяет `retro_stats.py` и `retro_blocks.py`.
- `docs/retro/scripts/retro_leftovers.py` ищет хвосты чеклиста закрытия по состоянию репозиториев: слитые ветки на сервере и локально, worktree, SDD workspace закрытых задач, неархивные changes, отставший или грязный main.
- Корпус хуков разбит на сегменты со своей раскладкой; записи `known` — известные ошибки хуков (`KNOWN-FIXED` — пора снять пометку), doctor показывает их число. `hook_blocks_export.py --since --id-prefix` выгружает блокировки среза, `hook_cases_build.py` собирает корпус из `hook-labels-<дата>.json`.
- `docs/retro/2026-10-11.metrics.json` — метрики ретро 11.10, перенесённые из таблицы. Блокировки — 76 ошибок хука, из них 72 ложные (O-1). Ходы и токены 11.10 считались по строкам и помечены как несравнимые.
## 0.5.1 — 2026-10-24 ## 0.5.1 — 2026-10-24
- Worktree живут в `<repo>/.claude/worktrees/<slug>`, а не в соседнем каталоге: их создаёт `EnterWorktree` с именем, ветка `worktree-<slug>` переименовывается в `feat/<id>-<slug>` (#25). Вход в worktree вне `.claude/worktrees/` спрашивает подтверждение при любых allow-правилах (Claude Code ≥ 2.1.206). Описано в `roles/dev.md` и `HARNESS.md`. - Worktree живут в `<repo>/.claude/worktrees/<slug>`, а не в соседнем каталоге: их создаёт `EnterWorktree` с именем, ветка `worktree-<slug>` переименовывается в `feat/<id>-<slug>` (#25). Вход в worktree вне `.claude/worktrees/` спрашивает подтверждение при любых allow-правилах (Claude Code ≥ 2.1.206). Описано в `roles/dev.md` и `HARNESS.md`.
- `.claude/worktrees/` — в `.gitignore` проектов: `adopt-project.sh`, `project-template/gitignore`, этот репозиторий. `doctor.sh`: `WARN .claude/worktrees/ не в .gitignore`. - `.claude/worktrees/` — в `.gitignore` проектов: `adopt-project.sh`, `project-template/gitignore`, этот репозиторий. `doctor.sh`: `WARN .claude/worktrees/ не в .gitignore`.

View File

@ -18,7 +18,7 @@
- `roles/` — общий слой ролей (формат: четыре раздела, см. спеку ролей), `skills/role/` — skill `/role`, `agents/briefing.md` — агент выжимки, `link-roles.sh` — симлинки workspace → репозитории направлений. Ставятся `install.sh` в `~/.claude/{roles,skills/role,agents,harness}`. - `roles/` — общий слой ролей (формат: четыре раздела, см. спеку ролей), `skills/role/` — skill `/role`, `agents/briefing.md` — агент выжимки, `link-roles.sh` — симлинки workspace → репозитории направлений. Ставятся `install.sh` в `~/.claude/{roles,skills/role,agents,harness}`.
- `tests/` — тесты пакета на bash: `run.sh` + `test-*.sh`, работают с источниками, `install.sh` ставят во временный `CLAUDE_CONFIG_DIR`. - `tests/` — тесты пакета на bash: `run.sh` + `test-*.sh`, работают с источниками, `install.sh` ставят во временный `CLAUDE_CONFIG_DIR`.
- `caveman/config.json` — выключенный по умолчанию caveman. `examples/` — реальный `CLAUDE.md` проекта-пилота. - `caveman/config.json` — выключенный по умолчанию caveman. `examples/` — реальный `CLAUDE.md` проекта-пилота.
- `docs/HARNESS.md` — как устроен цикл фичи, SDD, маркировка, закрытие. `docs/PILOT-2026-09-27.md` — пилот. `docs/retro/` — ретроспектива 11.10.2026 и скрипты подсчёта по транскриптам. `docs/superpowers/specs/` — спеки фич обвязки, `docs/superpowers/plans/` — планы. - `docs/HARNESS.md` — как устроен цикл фичи, SDD, маркировка, закрытие. `docs/PILOT-2026-09-27.md` — пилот. `docs/retro/` — ретроспективы, их метрики (`<дата>.metrics.json`) и скрипты: `retro_metrics.py` (срез метрик и поправки владельца), `retro_leftovers.py` (хвосты закрытия), `hook_blocks_export.py` и `hook_cases_build.py` (блокировки → корпус); проектный skill `/harness-retro` — `.claude/skills/harness-retro/`. `docs/superpowers/specs/` — спеки фич обвязки, `docs/superpowers/plans/` — планы.
- Связь слоёв: хуки читают JSON вызова из stdin и состояние git проекта; скрипты бутстрапа читают `project-template/`; `doctor.sh` читает установленные копии в `~/.claude`, не репозиторий. - Связь слоёв: хуки читают JSON вызова из stdin и состояние git проекта; скрипты бутстрапа читают `project-template/`; `doctor.sh` читает установленные копии в `~/.claude`, не репозиторий.
## Команды ## Команды
@ -28,13 +28,13 @@
- `bash skills/harness-doctor/scripts/doctor.sh [проект]` — отчёт и самотест хуков. Только чтение. - `bash skills/harness-doctor/scripts/doctor.sh [проект]` — отчёт и самотест хуков. Только чтение.
- `bash adopt-project.sh <папка> [--no-merge] [--push]` — подключить существующий проект. Из терминала, не из Claude Code. - `bash adopt-project.sh <папка> [--no-merge] [--push]` — подключить существующий проект. Из терминала, не из Claude Code.
- `bash new-project.sh <папка> [ssh-url]` — каркас нового проекта. - `bash new-project.sh <папка> [ssh-url]` — каркас нового проекта.
- `python3 docs/retro/scripts/retro_stats.py <каталог транскриптов>` и `retro_blocks.py` — статистика сессий и блокировок хуков для ретро; `hook_blocks_export.py <каталог> [...]` — выгрузка блокировок в JSONL (сырьё для тестов хуков); `hook_cases_build.py > skills/harness-doctor/fixtures/hook-cases.jsonl` — сборка размеченного корпуса из выгрузки. - `/harness-retro <папка проекта> [--since ГГГГ-ММ-ДД]` — ретро обвязки (в сессии в этом репозитории). Под ним: `python3 docs/retro/scripts/retro_metrics.py <папка> [--since …] [--out …] [--markdown --prev …] [--corrections]` — срез метрик; `retro_leftovers.py <папка> [--fetch] [--markdown]` — хвосты закрытия; `hook_blocks_export.py [--since …] [--id-prefix …] <каталог> [...]` — выгрузка блокировок в JSONL; `hook_cases_build.py > skills/harness-doctor/fixtures/hook-cases.jsonl` — сборка корпуса из разметки.
- `python3 skills/harness-doctor/scripts/hook_corpus.py hooks [--summary]` — корпус хуков по источникам. `python3 hooks/harness_target.py targets '<команда>' [cwd]` — какие цели записи видит хук. - `python3 skills/harness-doctor/scripts/hook_corpus.py hooks [--summary]` — корпус хуков по источникам. `python3 hooks/harness_target.py targets '<команда>' [cwd]` — какие цели записи видит хук.
## Тесты ## Тесты
- `tests/run.sh` — автоматические тесты источников (хуки через фиктивный JSON, скрипты во временных каталогах, размеры). Самотест хуков в `doctor.sh` проверяет установленные копии; перед ним `bash install.sh`. - `tests/run.sh` — автоматические тесты источников (хуки через фиктивный JSON, скрипты во временных каталогах, размеры). Самотест хуков в `doctor.sh` проверяет установленные копии; перед ним `bash install.sh`.
- Ручная проверка хука: на main в любом репозитории попросить агента создать файл, ожидается `BLOCKED`. - Ручная проверка хука: на main в любом репозитории попросить агента создать файл, ожидается `BLOCKED`.
- Хуки: `test-hook-parse` — разбор команды на цели; `test-hook-guard-main`, `test-hook-require-plan` — сценарии спеки `hook-guards` в песочнице из настоящих репозиториев; `test-hook-corpus` — корпус реальных команд (assert на запись, корпус должен совпадать со сборкой `hook_cases_build.py`); `test-hook-export` — экстрактор. Плюс `role-hint` и скрипты `install.sh`, `doctor.sh`, `link-roles.sh`. - Хуки: `test-hook-parse` — разбор команды на цели; `test-hook-guard-main`, `test-hook-require-plan` — сценарии спеки `hook-guards` в песочнице из настоящих репозиториев; `test-hook-corpus` — корпус реальных команд (assert на запись, корпус должен совпадать со сборкой `hook_cases_build.py`); `test-hook-export` — экстрактор. Ретро: `test-retro-metrics` (синтетические транскрипты), `test-retro-leftovers` (песочница репозиториев), `test-retro-skill`. Плюс `role-hint` и скрипты `install.sh`, `doctor.sh`, `link-roles.sh`.
- Правишь разбор или вердикт — сначала случай в `test-hook-parse`/`test-hook-guard-main`; меняется ожидаемый вердикт реальной команды — разметка в `hook_cases_build.py` и пересборка корпуса. - Правишь разбор или вердикт — сначала случай в `test-hook-parse`/`test-hook-guard-main`; меняется ожидаемый вердикт реальной команды — разметка в `hook_cases_build.py` и пересборка корпуса.
## Текущая работа ## Текущая работа
@ -44,7 +44,8 @@
- Сделано: B3 — allow-правило `git push origin --delete` печатают adopt/new-project, проверяет doctor (issue #22, 0.4.2). B4 закрыт в #6 (решение владельца 1). B10 — workspace-хук awp-ca, в части 2 issue #2. - Сделано: B3 — allow-правило `git push origin --delete` печатают adopt/new-project, проверяет doctor (issue #22, 0.4.2). B4 закрыт в #6 (решение владельца 1). B10 — workspace-хук awp-ca, в части 2 issue #2.
- Сделано: allow-правила цикла фичи ставит `install.sh` (issue #24, 0.5.0; пересмотр решения владельца 2). - Сделано: allow-правила цикла фичи ставит `install.sh` (issue #24, 0.5.0; пересмотр решения владельца 2).
- Сделано: worktree — `EnterWorktree` в `<repo>/.claude/worktrees/<slug>`, вход без подтверждения (issue #25, 0.5.1). Для awp-ca вопрос открыт в части 2 issue #2. - Сделано: worktree — `EnterWorktree` в `<repo>/.claude/worktrees/<slug>`, вход без подтверждения (issue #25, 0.5.1). Для awp-ca вопрос открыт в части 2 issue #2.
- Дальше: B12. - Сделано: B12 — `/harness-retro`: срез метрик, поправки владельца, хвосты закрытия, корпус по сегментам (issue #29, 0.6.0), спека — `openspec/specs/harness-retro/`.
- Дальше: первое ретро `/harness-retro` по полигону — на машине с awp-ca; остальные пункты бэклога — по мере надобности.
## OpenSpec и superpowers ## OpenSpec и superpowers
- Вход в любую задачу — superpowers:brainstorming. Skills openspec-propose и openspec-explore сами по себе не запускать. - Вход в любую задачу — superpowers:brainstorming. Skills openspec-propose и openspec-explore сами по себе не запускать.

View File

@ -38,6 +38,8 @@ link-roles.sh симлинки ролей/агентов из worksp
project-template/workspace/ CLAUDE.md workspace с таблицей репозиториев, roles/dev.md project-template/workspace/ CLAUDE.md workspace с таблицей репозиториев, roles/dev.md
project-template/role-repo/ репозиторий направления: CLAUDE.md, ROLE.md, notes/README.md project-template/role-repo/ репозиторий направления: CLAUDE.md, ROLE.md, notes/README.md
tests/run.sh тесты пакета (bash): bash tests/run.sh tests/run.sh тесты пакета (bash): bash tests/run.sh
.claude/skills/harness-retro/ проектный skill /harness-retro: ретро обвязки по транскриптам проекта (запуск из этого репозитория)
docs/retro/scripts/ retro_metrics.py (срез метрик), retro_leftovers.py (хвосты закрытия), выгрузка и разметка блокировок
docs/HARNESS.md как это работает: слои, цикл фичи, SDD, дисциплина закрытия docs/HARNESS.md как это работает: слои, цикл фичи, SDD, дисциплина закрытия
docs/PILOT-2026-09-27.md результаты пилота и находки docs/PILOT-2026-09-27.md результаты пилота и находки
``` ```

View File

@ -99,6 +99,20 @@ Workspace из нескольких репозиториев (не git, cwd вс
Политика памяти (глобальный CLAUDE.md): состояние задачи — в трекере; уроки, решения владельца и рецепты — в `notes/` направления и памяти агентов (в git); в локальной auto-memory — только ловушки машины и индекс со ссылками. При закрытии файл auto-memory задачи сворачивается в строку `registry-closed.md`. Политика памяти (глобальный CLAUDE.md): состояние задачи — в трекере; уроки, решения владельца и рецепты — в `notes/` направления и памяти агентов (в git); в локальной auto-memory — только ловушки машины и индекс со ссылками. При закрытии файл auto-memory задачи сворачивается в строку `registry-closed.md`.
## Ретро
Регулярное ретро — `/harness-retro <папка проекта или workspace> [--since ГГГГ-ММ-ДД]`, проектный skill этого репозитория (`.claude/skills/harness-retro/`). Запускается в сессии в репозитории обвязки по команде владельца после среза и идёт по циклу фичи: issue, worktree, PR. Результат — черновик `docs/retro/<дата>.md` по шаблону и `docs/retro/<дата>.metrics.json`.
- `retro_metrics.py` — срез метрик:
- сессии, промпты, инструменты, роли, сабагенты, токены, стартовый контекст;
- блокировки и отказы;
- codegraph против чтения кода;
- память.
Сравнение с прошлым ретро — таблица «Цифры». Кандидаты в поправки владельца по процессу — `--corrections`.
- Блокировки среза агент размечает по спеке `hook-guards` в `docs/retro/fixtures/hook-labels-<дата>.json`, и они становятся сегментом регрессионного корпуса. Запись с `known` — известная ошибка хуков; doctor показывает их число.
- `retro_leftovers.py` — хвосты чеклиста закрытия по состоянию репозиториев. Он только подсказывает, как убрать; убирает владелец.
## Стоимость ## Стоимость
Bootstrap superpowers (~3 КБ) вставляется на каждом старте и после компакции. SDD стоит свежий контекст на задачу и на ревью; подробный план с готовым кодом позволяет отдать большинство задач haiku. Для планов до 5 задач дешевле `executing-plans` (Native): один контекст, одно ревью в конце. Bootstrap superpowers (~3 КБ) вставляется на каждом старте и после компакции. SDD стоит свежий контекст на задачу и на ревью; подробный план с готовым кодом позволяет отдать большинство задач haiku. Для планов до 5 задач дешевле `executing-plans` (Native): один контекст, одно ревью в конце.

View File

@ -0,0 +1,44 @@
{
"schema": 1,
"period": {"since": "2026-10-02", "until": "2026-10-11"},
"project": "/Users/andrey.epifancev/projects/awp-ca",
"sources": {
"dirs": ["-Users-andrey-epifancev-projects-awp-ca"],
"sessions": 44,
"bytes": 881852416,
"bad_lines": null,
"note": "перенесено вручную из таблицы «Цифры» docs/retro/2026-10-11.md (#29); 47 транскриптов, 841 МБ"
},
"metrics": {
"sessions": 44,
"prompts": 613,
"interrupts": null,
"turns": {"value": 12121, "note": "по строкам транскрипта, не по сообщениям"},
"tool_calls": 6583,
"tools": {"Bash": 4302, "Playwright": 1100, "Read": 346, "Write": 271, "Edit": 233},
"roles": {"dev": 32, "admin": 7, "qa": 5},
"subagents": 61,
"subagent_models": {"sonnet": 47, "opus": 9, "не указана": 5},
"skills": {"brainstorming": 19, "writing-plans": 14, "systematic-debugging": 8, "subagent-driven-development": 4,
"executing-plans": 3, "using-git-worktrees": 3, "test-driven-development": 1,
"finishing-a-development-branch": 1},
"tokens": {
"output": {"value": 18300000, "note": "по строкам транскрипта"},
"cache_read": {"value": 3820000000, "note": "по строкам транскрипта"},
"cache_write": {"value": 145000000, "note": "по строкам транскрипта"}
},
"compactions": 4,
"start_context": {"median": 56000, "min": 52000, "max": 61000},
"hook_blocks": 76,
"hook_blocks_by_hook": null,
"false_blocks": {"labelled": 76, "skipped": 0, "false": 72, "share": 0.95, "fixed": 72, "known": 0},
"classifier_denials": 6,
"classifier_by_reason": {"Git Destructive": 2, "Auto-Mode Bypass": 2, "Interfere With Workloads": 1, "safety-check": 1},
"owner_denials": 6,
"codegraph": {"init": 43, "explore": 15, "code_reads": null, "explore_share": null},
"corrections": null,
"recall": 9,
"memory": {"index_bytes": null, "files": 47, "bytes": 183296},
"cd_main": {"retired": "хуки 0.4.0 не зависят от cwd (RT-2); считалось по именам репозиториев awp-ca, было 84"}
}
}

View File

@ -1,140 +0,0 @@
#!/usr/bin/env python3
"""Классификация блокировок хуков, отказов auto mode, стартового контекста и ручных напоминаний."""
import json, sys, glob, os, re, collections
d = sys.argv[1]
files = sorted(glob.glob(os.path.join(d, "*.jsonl")))
RE_REAL = re.compile(r'(^|[^<\d])>{1,2}[^&]|\btee\b|\bsed\s+-i|\bmv\b|\bcp\b|\brm\b|\btouch\b|\bmkdir\b|\bgit\s+(add|commit|push|apply|stash\s+pop)\b|\bnpm\s+(i|install|init)\b|\bpip\s+install\b')
RE_STDERR = re.compile(r'\d>{1,2}\s*/dev/null|\d>&\d')
RE_HEREDOC_PY = re.compile(r"<<\s*'?(PY|EOF)")
RE_HOOK_ERROR = re.compile(r"\s*PreToolUse:\S+ hook error") # только ошибка хука, не вывод команды со словом BLOCKED
blocks = collections.Counter()
by_hook = collections.Counter()
by_session = collections.Counter()
examples = collections.defaultdict(list)
denials = collections.Counter()
denial_ex = []
start_ctx = []
recall_prompts = []
codegraph_init = 0
cd_main = 0
n_prompts_total = 0
for f in files:
sid = os.path.basename(f)[:8]
pending = {} # tool_use_id -> (name, input)
first_usage = None
title = ""
for line in open(f, encoding="utf-8", errors="replace"):
try:
j = json.loads(line)
except Exception:
continue
t = j.get("type")
if t == "ai-title" and not title:
title = j.get("aiTitle", "")
if t == "assistant":
m = j.get("message", {})
u = m.get("usage") or {}
if first_usage is None and u:
first_usage = (u.get("input_tokens", 0), u.get("cache_creation_input_tokens", 0), u.get("cache_read_input_tokens", 0))
for b in m.get("content", []) or []:
if isinstance(b, dict) and b.get("type") == "tool_use":
pending[b.get("id")] = (b.get("name"), b.get("input") or {})
if b.get("name") == "Bash":
cmd = (b.get("input") or {}).get("command", "")
if "codegraph init" in cmd:
codegraph_init += 1
if re.search(r"\bcd\s+\S*(arm-core|docs|tabular-parser|scan-gateway|arm-web)/?\s*($|;|&&)", cmd) and "/.wt/" not in cmd:
cd_main += 1
if t == "user":
m = j.get("message", {})
cont = m.get("content", "")
if isinstance(cont, str) and not cont.startswith("<") and not j.get("isMeta"):
n_prompts_total += 1
if re.search(r"памят|перечитай|сверься|контекст|вспомни|прочитай .*md", cont, re.I):
recall_prompts.append((sid, cont[:140].replace("\n", " ")))
if isinstance(cont, list):
for b in cont:
if not (isinstance(b, dict) and b.get("type") == "tool_result"):
continue
txt = b.get("content", "")
if isinstance(txt, list):
txt = "\n".join(x.get("text", "") for x in txt if isinstance(x, dict))
name, inp = pending.get(b.get("tool_use_id"), ("?", {}))
if RE_HOOK_ERROR.match(txt) and "BLOCKED" in txt:
if name not in ("Bash", "Edit", "Write", "MultiEdit"):
continue
if "[guard-main]" in txt:
hook = "guard-main(Bash)" if name == "Bash" else "guard-main(Edit/Write)"
elif "[require-plan]" in txt:
hook = "require-plan"
elif "shell write" in txt:
hook = "guard-main(Bash)"
elif "you are on" in txt:
hook = "guard-main(Edit/Write)"
elif "without a plan" in txt:
hook = "require-plan"
elif "commit/push в main" in txt or "ветке main" in txt or "reference-old-arm" in txt or "docs/source" in txt:
hook = "workspace-guard"
else:
hook = "other"
by_hook[hook] += 1
by_session[sid] += 1
cmd = inp.get("command", "") if name == "Bash" else inp.get("file_path", "")
if name == "Bash":
if RE_STDERR.search(cmd) and not RE_REAL.sub("", cmd) != cmd:
cls = "stderr-redirect-only"
else:
stripped = RE_STDERR.sub("", cmd)
if RE_REAL.search(stripped):
mm = RE_REAL.search(stripped).group(0)
if re.match(r"^\s*(cat|python3|grep|ls|find|sed -n|head|git (log|status|diff|show|branch|worktree list))", cmd) and ("mkdir" not in cmd and "git commit" not in cmd and "git push" not in cmd and "git add" not in cmd and "rm " not in cmd):
cls = "read-only-but-matched:" + mm.strip()[:12]
else:
cls = "real-write:" + mm.strip()[:12]
else:
cls = "no-pattern(workspace or cwd)"
else:
cls = name
blocks[(hook, cls)] += 1
if len(examples[(hook, cls)]) < 3:
examples[(hook, cls)].append(f"[{sid}] {cmd[:150]!r}")
if "denied by the Claude Code auto mode classifier" in txt or "denied by a built-in Claude Code safety check" in txt or "The user doesn't want to proceed" in txt:
r = re.search(r"Reason: \[([^\]]+)\]", txt)
key = r.group(1) if r else ("safety-check" if "safety check" in txt else "user-rejected")
denials[key] += 1
cmd = inp.get("command", "") if name == "Bash" else name
denial_ex.append(f"[{sid}] {key}: {cmd[:140]!r}")
if first_usage:
start_ctx.append((sid, title[:30], sum(first_usage), first_usage))
print("== Блокировки по хукам")
for k, v in by_hook.most_common():
print(f"{k}: {v}")
print("\n== Блокировки по классу")
for k, v in sorted(blocks.items(), key=lambda x: -x[1]):
print(f"{v:3d} {k[0]} / {k[1]}")
for e in examples[k]:
print(" ", e)
print("\n== Сессии с наибольшим числом блокировок")
for k, v in by_session.most_common(8):
print(f"{k}: {v}")
print("\n== Отказы auto mode / safety / пользователь")
for k, v in denials.most_common():
print(f"{k}: {v}")
for e in denial_ex:
print(" ", e)
print("\n== Стартовый контекст (первый ответ модели): input+cache_create+cache_read")
start_ctx.sort(key=lambda x: -x[2])
for s, t, tot, fu in start_ctx:
print(f"{s} {tot:7d} (in={fu[0]} create={fu[1]} read={fu[2]}) {t}")
vals = [x[2] for x in start_ctx if x[2] > 0]
vals.sort()
print(f"median={vals[len(vals)//2]} min={vals[0]} max={vals[-1]} n={len(vals)}")
print(f"\n== codegraph init в командах: {codegraph_init}; cd в основной checkout (не .wt): {cd_main}")
print(f"\n== Промпты владельца с просьбой перечитать память/контекст: {len(recall_prompts)} из {n_prompts_total}")
for s, p in recall_prompts:
print(f" [{s}] {p}")

View File

@ -1,159 +0,0 @@
#!/usr/bin/env python3
"""Статистика по транскриптам сессий Claude Code для ретро обвязки.
Только чтение. Вывод: таблица по сессиям + агрегаты + примеры блокировок."""
import json, sys, glob, os, re, collections, datetime
d = sys.argv[1]
files = sorted(glob.glob(os.path.join(d, "*.jsonl")))
rows = []
agg = collections.Counter()
blocked_examples = collections.defaultdict(list)
denied_examples = []
system_subtypes = collections.Counter()
hook_names = collections.Counter()
skills = collections.Counter()
agent_models = collections.Counter()
agent_types = collections.Counter()
first_prompts = {}
PAT_BLOCK = re.compile(r"BLOCKED[^\n]{0,160}")
PAT_DENY = re.compile(r"(Auto-Mode Bypass|permission denied|user denied|denied by|The user doesn't want|rejected this tool)", re.I)
def text_of(content):
if isinstance(content, str):
return content
out = []
if isinstance(content, list):
for b in content:
if isinstance(b, dict):
if b.get("type") == "text":
out.append(b.get("text", ""))
elif b.get("type") == "tool_result":
out.append(text_of(b.get("content", "")))
return "\n".join(out)
for f in files:
sid = os.path.basename(f)[:8]
c = collections.Counter()
title = ""
pm = ""
t0 = t1 = None
usage_in = usage_out = cache_r = cache_c = 0
human_prompts = 0
first_prompt = ""
for line in open(f, encoding="utf-8", errors="replace"):
try:
j = json.loads(line)
except Exception:
continue
t = j.get("type")
if t == "ai-title" and not title:
title = j.get("aiTitle", "")
if t == "permission-mode":
pm = j.get("permissionMode", "")
ts = j.get("timestamp")
if ts:
try:
tt = datetime.datetime.fromisoformat(ts.replace("Z", "+00:00"))
t0 = tt if t0 is None or tt < t0 else t0
t1 = tt if t1 is None or tt > t1 else t1
except Exception:
pass
if t == "system":
system_subtypes[j.get("subtype", "?")] += 1
if j.get("subtype") == "compact_boundary" or "compact" in str(j.get("subtype", "")):
c["compact"] += 1
hn = j.get("hookName") or j.get("hook_name")
if hn:
hook_names[hn] += 1
if t == "user":
m = j.get("message", {})
cont = m.get("content", "")
if isinstance(cont, str):
if not cont.startswith("<") and j.get("userType", "external") == "external" and not j.get("isMeta"):
human_prompts += 1
if not first_prompt:
first_prompt = cont[:120].replace("\n", " ")
elif isinstance(cont, list):
has_tr = any(isinstance(b, dict) and b.get("type") == "tool_result" for b in cont)
if not has_tr and not j.get("isMeta"):
txt = text_of(cont)
if txt and not txt.startswith("<"):
human_prompts += 1
if not first_prompt:
first_prompt = txt[:120].replace("\n", " ")
for b in cont:
if isinstance(b, dict) and b.get("type") == "tool_result":
txt = text_of(b.get("content", ""))
for mm in PAT_BLOCK.findall(txt):
c["blocked"] += 1
key = re.sub(r"\s+", " ", mm)[:90]
if len(blocked_examples[key]) < 2:
blocked_examples[key].append(sid)
if PAT_DENY.search(txt):
c["denied"] += 1
if len(denied_examples) < 15:
denied_examples.append((sid, re.sub(r"\s+", " ", txt)[:160]))
# hook feedback in attachments / text
txt = text_of(cont)
if "BLOCKED" in txt and isinstance(cont, str):
c["blocked_text"] += 1
if t == "assistant":
m = j.get("message", {})
u = m.get("usage", {}) or {}
usage_in += u.get("input_tokens", 0) or 0
usage_out += u.get("output_tokens", 0) or 0
cache_r += u.get("cache_read_input_tokens", 0) or 0
cache_c += u.get("cache_creation_input_tokens", 0) or 0
c["assistant"] += 1
for b in m.get("content", []) or []:
if isinstance(b, dict) and b.get("type") == "tool_use":
n = b.get("name", "")
c["tool"] += 1
c["t:" + n] += 1
agg["t:" + n] += 1
inp = b.get("input", {}) or {}
if n in ("Agent", "Task"):
agent_models[inp.get("model", "default")] += 1
agent_types[inp.get("subagent_type", "?")] += 1
if n == "Skill":
skills[inp.get("skill", "?")] += 1
dur = (t1 - t0).total_seconds() / 3600 if t0 and t1 else 0
rows.append(dict(sid=sid, date=t0.strftime("%m-%d") if t0 else "?", hours=round(dur, 1), title=title[:38], pm=pm,
prompts=human_prompts, asst=c["assistant"], tools=c["tool"], agents=c["t:Agent"] + c["t:Task"],
blocked=c["blocked"], denied=c["denied"], compact=c["compact"],
out_k=usage_out // 1000, cache_r_m=round(cache_r / 1e6, 1), first=first_prompt[:70]))
for k in ("assistant", "tool", "blocked", "denied", "compact"):
agg[k] += c[k]
agg["prompts"] += human_prompts
agg["out"] += usage_out
agg["cache_r"] += cache_r
agg["cache_c"] += cache_c
agg["hours"] += dur
rows.sort(key=lambda r: r["date"])
hdr = ["sid", "date", "hours", "pm", "prompts", "asst", "tools", "agents", "blocked", "denied", "compact", "out_k", "cache_r_m", "title"]
print("\t".join(hdr))
for r in rows:
print("\t".join(str(r[h]) for h in hdr))
print("\n== Первые промпты сессий")
for r in rows:
print(f"{r['date']} {r['sid']} | {r['first']}")
print("\n== Агрегаты")
for k in ("prompts", "assistant", "tool", "blocked", "denied", "compact"):
print(f"{k}: {agg[k]}")
print(f"hours: {agg['hours']:.0f}; output tokens: {agg['out']/1e6:.1f}M; cache read: {agg['cache_r']/1e9:.2f}G; cache create: {agg['cache_c']/1e6:.0f}M")
print("\n== Инструменты (top 25)")
for k, v in sorted(((k, v) for k, v in agg.items() if k.startswith("t:")), key=lambda x: -x[1])[:25]:
print(f"{k[2:]}: {v}")
print("\n== Skills"); [print(f"{k}: {v}") for k, v in skills.most_common()]
print("\n== Модели сабагентов"); [print(f"{k}: {v}") for k, v in agent_models.most_common()]
print("\n== Типы сабагентов"); [print(f"{k}: {v}") for k, v in agent_types.most_common()]
print("\n== system subtypes"); [print(f"{k}: {v}") for k, v in system_subtypes.most_common(15)]
print("\n== hookName в system"); [print(f"{k}: {v}") for k, v in hook_names.most_common(15)]
print("\n== Блокировки хуков (уникальные тексты, top 40)")
for k, v in sorted(blocked_examples.items(), key=lambda x: -len(x[1]))[:40]:
print(f"[{','.join(v)}] {k}")
print("\n== Отказы (примеры)")
for s, e in denied_examples:
print(f"[{s}] {e}")

32
tests/test-retro-skill.sh Normal file
View File

@ -0,0 +1,32 @@
# /harness-retro: проектный skill, шаблон, метрики 11.10, удалённые скрипты (#29).
S="$ROOT/.claude/skills/harness-retro"
assert_file "SKILL.md" "$S/SKILL.md"
assert_grep "SKILL.md: name" '^name: harness-retro$' "$S/SKILL.md"
for step in 'retro_metrics\.py' 'hook_blocks_export\.py' 'hook_cases_build\.py' 'retro_leftovers\.py' '--corrections' 'EnterWorktree' 'tea pulls create'; do
assert_grep "SKILL.md упоминает $step" "$step" "$S/SKILL.md"
done
assert_file "template.md" "$S/template.md"
for sec in "## 1. Цифры" "## 2. Находки" "## 3. Поправки владельца" "## 4. Что сработало" "## 5. Бэклог" "## 6. Решения владельца" "## 7. Что не измерялось"; do
assert_grep "шаблон: $sec" "^$sec" "$S/template.md"
done
J="$ROOT/docs/retro/2026-10-11.metrics.json"
assert_eq "метрики 11.10: схема, cd снят, ложные" "$(python3 -c 'import json,sys; d=json.load(open(sys.argv[1])); m=d["metrics"]; print(d["schema"], "retired" in m["cd_main"], m["false_blocks"]["false"], m["hook_blocks"])' "$J")" "1 True 72 76"
out=$(python3 - "$ROOT/docs/retro/scripts" "$J" <<'PY'
import json, sys
sys.path.insert(0, sys.argv[1])
import retro_metrics
prev = json.load(open(sys.argv[2]))
doc = {"metrics": {"sessions": 3, "turns": 7, "hook_blocks": 1}, "period": {"since": "2026-10-11", "until": "2026-10-24"},
"sources": {"sessions": 3, "dirs": [], "bad_lines": 0}}
print(retro_metrics.to_markdown(doc, prev))
PY
)
assert_grep "сравнение с 11.10: сессии" '^\| Сессий \| 3 \| 44 \| -41 \|$' <(printf '%s' "$out")
assert_grep "сравнение с 11.10: блокировки" '^\| Блокировок хуков \| 1 \| 76 \| -75 \|$' <(printf '%s' "$out")
assert_grep "сравнение с 11.10: cd снят" '^\| cd в основной checkout на main \| — \| — \| не считается' <(printf '%s' "$out")
assert_grep "сравнение с 11.10: ходы считались по строкам" '^\| Ходов модели \| 7 \| 12 121 \(по строкам' <(printf '%s' "$out")
assert_eq "retro_stats.py удалён" "$([ -e "$ROOT/docs/retro/scripts/retro_stats.py" ] && echo есть || echo нет)" "нет"
assert_eq "retro_blocks.py удалён" "$([ -e "$ROOT/docs/retro/scripts/retro_blocks.py" ] && echo есть || echo нет)" "нет"
assert_eq "CLAUDE.md без ссылок на удалённые скрипты" "$(grep -c 'retro_stats\|retro_blocks' "$ROOT/CLAUDE.md")" "0"
assert_grep "CLAUDE.md: retro_metrics в командах" 'retro_metrics\.py' "$ROOT/CLAUDE.md"
assert_grep "HARNESS.md: раздел Ретро" '^## Ретро' "$ROOT/docs/HARNESS.md"