docs: ретроспектива обвязки 11.10.2026 по полигону awp-ca
Находки RT-1…RT-20, бэклог B1…B12, решения владельцу; скрипты подсчёта по транскриптам (retro_stats.py, retro_blocks.py) как основа регулярного ретро. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
6ab7dbe774
commit
0733ffd9c9
183
docs/retro/2026-10-11.md
Normal file
183
docs/retro/2026-10-11.md
Normal file
@ -0,0 +1,183 @@
|
||||
# Ретроспектива обвязки claude-harness — 11.10.2026
|
||||
|
||||
Период: 02.10–11.10.2026, полигон — workspace `awp-ca` (АРМ КА: пять git-репозиториев рядом, workspace сам не git).
|
||||
Предыдущая точка сравнения — `PILOT-2026-09-27.md` (один репозиторий, одна фича).
|
||||
|
||||
Источники: 47 транскриптов сессий Claude Code (`~/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/*.jsonl`, 841 МБ),
|
||||
47 файлов auto-memory того же проекта (179 КБ), код хуков, `HARNESS.md`, `PILOT-2026-09-27.md`, `CHANGELOG.md`.
|
||||
Количественная часть считалась скриптами `scripts/retro_stats.py` и `scripts/retro_blocks.py` (только чтение транскриптов).
|
||||
Качественная часть — прочтение всей памяти целиком.
|
||||
|
||||
Маркировка находок ретро: `RT-n`. Приоритет: P1 — теряет время каждую неделю или ломает процесс; P2 — трение; P3 — наблюдение.
|
||||
Статус находки проставляется при закрытии бэклога (fixed / deferred / rejected), как и для любых других маркеров обвязки.
|
||||
|
||||
## 1. Цифры
|
||||
|
||||
| Показатель | Значение |
|
||||
|---|---|
|
||||
| Сессий (непустых) | 44 за 10 дней; 3 сессии больше 20 МБ транскрипта |
|
||||
| Промптов владельца | 613 |
|
||||
| Ходов модели / вызовов инструментов | 12 121 / 6 583 (Bash 4 302, Playwright ≈1 100, Read 346, Write 271, Edit 233) |
|
||||
| Запусков сабагентов | 61 (sonnet 47, opus 9, модель не указана 5) |
|
||||
| Вызовов skills | brainstorming 19, writing-plans 14, systematic-debugging 8, SDD 4, executing-plans 3, using-git-worktrees 3, TDD 1, finishing-a-development-branch 1, verification-before-completion 0, requesting/receiving-code-review 0 |
|
||||
| Токены | вывод 18,3 М; чтение из кэша 3,82 Г; запись в кэш 145 М |
|
||||
| Компакций контекста | 4 (в двух сессиях) |
|
||||
| Стартовый контекст до первого промпта | медиана 56 К токенов (мин 52 К, макс 61 К); наши инструкции ≈39 КБ текста ≈ 13–16 К токенов, остальное — системный промпт и схемы инструментов |
|
||||
| Блокировок хуков | 103 (guard-main Bash 91, guard-main Edit/Write 8, require-plan 3, прочие 1) |
|
||||
| Отказов классификатора auto mode | 6 (Git Destructive 2, Auto-Mode Bypass 2, Interfere With Workloads 1, safety-check 1); отказов владельца — 6 |
|
||||
| `codegraph init` в командах / вызовов `codegraph_explore` | 43 / 15 |
|
||||
| `cd` в основной checkout на main (не `.wt/`) | 84 |
|
||||
| Промптов «перечитай память/контекст» | 9 из 613 |
|
||||
|
||||
Сессии по ролям (эвристика по инструментам и первым промптам): разработка — 32 сессии, 377 промптов; администрирование
|
||||
(Jira, Гант, спринт) — 7 сессий, 122 промпта, все за 10–11.10; QA/фронт (Playwright > 30 вызовов) — 5 сессий, 81 промпт.
|
||||
Администрирование и QA — треть промптов, при этом вся обвязка и 44 из 47 файлов памяти написаны под разработчика.
|
||||
|
||||
## 2. Находки
|
||||
|
||||
### Хуки
|
||||
|
||||
**RT-1 (P1). Три четверти блокировок guard-main — ложные.** Из 91 блокировки Bash:
|
||||
36 — команда только чтения с `2>/dev/null` (регулярное выражение хука считает `N>` записью);
|
||||
27 — команда без единого паттерна записи в репозиторий (чтение памяти, `grep`, `cat`), заблокирована потому, что cwd оболочки
|
||||
стоял на main-checkout; 8 блокировок Edit/Write пришлись на файлы вне любого репозитория (scratchpad сессии, файлы памяти) —
|
||||
хук смотрит на cwd, а не на путь файла. Ещё около 20 — шаги самого процесса, которые хук не узнал: `git -C <repo> worktree add`
|
||||
(исключение есть только для литерала `git worktree`), `mkdir -p .wt/<задача>` в не-git workspace, архивный
|
||||
`git add openspec && git commit` (проверка staged идёт до `add`). Реальных перехватов записи в main за 10 дней — единицы
|
||||
(`sed -i` по docs на main в сессии d60ad490). Нарушений main при этом не было ни одного: хук защищает, но ценой шума.
|
||||
Корень: решение принимается по cwd оболочки, а не по цели записи. То же даёт RT-2.
|
||||
Источники: `retro_blocks.py`, память `workspace-hooks-and-worktrees`, `q1-2025-run`, `osv-inn-mapping-rework`, `console-files-state`.
|
||||
|
||||
**RT-2 (P1). Общий cwd оболочки с фоновым SDD-контроллером.** Один `cd` в основной checkout из главной сессии переводит
|
||||
контроллер и всех его implementer-ов в состояние `BLOCKED: shell write on main`, и они останавливаются. Правило «не cd, пока
|
||||
контроллер работает» записано в шести файлах памяти, то есть повторяется, а не устранено. Устраняется вместе с RT-1: хук,
|
||||
не зависящий от cwd, не ломается от чужого `cd`. Источники: память `slice-2a/2c/3a/3b-state`, `auto-confirm-64`, `arm-web-demo-prototype`.
|
||||
|
||||
**RT-3 (P1). Пункт 4 чеклиста закрытия (удалить ветку на сервере) стабильно не выполняется.** `git push origin --delete`
|
||||
блокируется guard-main при cwd на main и отклоняется классификатором auto mode как Git Destructive / Auto-Mode Bypass
|
||||
(4 из 6 отказов классификатора за период). Висящие серверные ветки упомянуты в 10 файлах памяти; владелец удаляет их руками.
|
||||
Найденный опытным путём обход (push из worktree на не-main ветке) нигде в обвязке не описан. Один раз агент обошёл хук
|
||||
через Gitea API, за что получил замечание (`feedback-no-harness-bypass`).
|
||||
|
||||
**RT-4 (P1). Правило «архив OpenSpec прямо в main» не работает в многорепозиторном workspace.** Глобальный guard-main
|
||||
пропускает openspec-only коммиты, workspace-хук awp-ca запрещает любой commit/push в main без исключений. Итог: около 15
|
||||
отдельных архивных PR, а поведение «то проходит, то нет» описано в памяти как урок. CLAUDE.md и фактическая практика расходятся.
|
||||
|
||||
**RT-5 (P2). Workspace-хук срабатывает на слово `commit`/`push` в любом месте команды**, включая `echo`, и на `reference-old-arm`
|
||||
в командах удаления симлинков при сносе worktree (владелец удаляет симлинки сам: `slice-3a-state`, `hotfolder-direct-write-state`,
|
||||
`core-33-golden-tests`).
|
||||
|
||||
**RT-6 (P2). У хуков нет регрессионных тестов.** `doctor.sh` делает самотест на нескольких случаях, но ни один из 103 реальных
|
||||
инцидентов не стал тестом. Транскрипты содержат готовый набор: команда + вердикт хука + факт, была ли это запись в main.
|
||||
|
||||
### SDD, сабагенты, классификатор
|
||||
|
||||
**RT-7 (P2). После одного отказа хука классификатор auto mode отклоняет у сабагента последующие коммиты как «Auto-Mode Bypass».**
|
||||
Владелец добавил allow-правила `git -C .wt/*`, `git add`, `git commit` вручную. Следствие RT-1: меньше ложных отказов — меньше каскадов.
|
||||
|
||||
**RT-8 (P2). Поднятый стенд ломает integration/e2e-тесты сабагентов** (общая queue group NATS на `recognition.result`).
|
||||
Повторилось минимум 5 раз, каждый раз оформлено как deferred-находка, системно не починено. Агенту гасить стенд нельзя
|
||||
(Interfere With Workloads), владелец делает через `!`. Это проектная находка, но обвязка должна давать место для «предусловий
|
||||
окружения перед SDD» (один раз проверить, а не пять раз откладывать).
|
||||
|
||||
**RT-9 (P3). haiku-implementer подписывает коммиты своим trailer вместо указанного в плане** (пилот, R-5/R-7 в `console-files-state`,
|
||||
O-5 в `slice-3a-state`); переписать нельзя из-за запрета force-push. Третий повтор одной находки.
|
||||
|
||||
**RT-10 (P3). Остановленный владельцем контроллер не возобновляется**, нужен новый контроллер «на добивку» (`slice-3b-state`).
|
||||
Ledger при этом работает: возобновление без потерь подтверждено и на пилоте, и в срезах.
|
||||
|
||||
**RT-11 (P3). Агент один раз спросил режим исполнения вопреки CLAUDE.md** (`feedback-execution-mode-from-claude-md`), и один раз
|
||||
начал задачу «не по харнесу» (сессия e08f3f0b, владелец перезапустил задачу). Процессные правила в 9 КБ глобального CLAUDE.md
|
||||
плюс 16 КБ проектного читаются не всегда; это аргумент за короткие ролевые файлы (RT-14).
|
||||
|
||||
### codegraph
|
||||
|
||||
**RT-12 (P2). 43 `codegraph init` против 15 вызовов `codegraph_explore` за 10 дней**; Bash — 4 302. В памяти нет ни одного
|
||||
свидетельства, что codegraph ускорил задачу; есть только «init сделан» как ритуал на каждый worktree и пустой ответ на пилоте.
|
||||
При этом правило «implementer вызывает codegraph_explore вместо grep/Read» стоит в глобальном CLAUDE.md. Сервер отстаёт по версии
|
||||
(1.6.0 при доступной 1.6.2).
|
||||
|
||||
### Память и контекст
|
||||
|
||||
**RT-13 (P1). Auto-memory стала журналом состояния срезов и дублирует Gitea.** 30 из 47 файлов имеют статус «ЗАКРЫТ» и
|
||||
фиксируют «issue #N закрыта, PR #M слит, ветка удалена» — то, что по замыслу обвязки помнит Gitea. Факты внутри файлов
|
||||
расходятся с индексом (`slice-4-state`: в индексе «ЗАКРЫТ», в файле «ждут мержа»; `logger-diagnose-state`: ветки «не удалены»
|
||||
в тексте, «удалены» в индексе). Один файл (`jira-backlog-actualization`, 15 КБ) — журнал версий артефакта v27…v37.
|
||||
Уникальные знания, ради которых память нужна, занимают малую долю: уроки по хукам, три файла фидбека владельца, рецепты QA
|
||||
(стенд, Playwright, скриншоты), структура Jira и ловушки REST, решения владельца по OQ.
|
||||
|
||||
**RT-14 (P1). Одна обвязка на четыре роли.** Разработчик, администратор проекта, аналитик требований и QA работают в одном
|
||||
проекте с одним CLAUDE.md, одним индексом памяти и одним процессом. В админ-сессии грузятся инварианты DBML и поток
|
||||
superpowers→Gitea→OpenSpec; в дев-сессии — Jira-память. Специализированных агентов в обвязке нет вовсе (ни `~/.claude/agents`,
|
||||
ни `.claude/agents`), SDD-контроллер описан текстом. Дизайн решения согласован отдельно (skill `/role`, файлы ролей, агенты
|
||||
с собственной памятью, реорганизация индекса); здесь фиксируется как находка.
|
||||
|
||||
**RT-15 (P3). Стартовый контекст 56 К токенов, из них наши инструкции 13–16 К.** Роли срежут 8–10 К в не-дев сессиях, но
|
||||
главный выигрыш от ролей — не токены, а отсутствие чужих правил. Платформенная часть (≈40 К) не наша.
|
||||
|
||||
### Процесс и чеклисты
|
||||
|
||||
**RT-16 (P2). Bounded-путь не требует обновления спек.** Bounded-PR arm-core #52/#53 изменили поведение API без правки
|
||||
`openspec/specs`; понадобился отдельный аудит (#60, четыре PR). Для bounded нужен один пункт: «меняется поведение API/схемы —
|
||||
спека обновляется в том же PR».
|
||||
|
||||
**RT-17 (P2). Сборка многорепозиторного worktree делается руками каждый раз**: симлинки fixtures/CLAUDE.md/.claude/reference-old-arm,
|
||||
`.bounded`, venv, симлинк `node_modules`, `codegraph init`. Ловушки: `worktree remove` отказывает из-за симлинка `node_modules`;
|
||||
относительный путь в `git worktree add` считается от репозитория, не от cwd. Около 25 bounded-задач и 7 срезов — столько же ручных сборок.
|
||||
|
||||
**RT-18 (P3). Skills завершения не используются**: finishing-a-development-branch 1 раз, verification-before-completion и
|
||||
requesting-code-review 0 раз. Закрытие и ревью идут по чеклисту CLAUDE.md и через SDD. Это не обязательно плохо, но значит,
|
||||
что превентивные проверки (verification) для bounded-задач не выполняются формально.
|
||||
|
||||
**RT-19 (P3). harness-check и adopt-project не оставили следов в awp-ca**: workspace не git, хук молчит. Для режима
|
||||
«workspace из нескольких репозиториев» у обвязки нет ни проверки, ни бутстрапа; `doctor.sh` его тоже не знает.
|
||||
|
||||
**RT-20 (P3). Follow-up issues растут быстрее, чем закрываются**: за период создано около 40 follow-up issue по чеклисту п. 6
|
||||
(core, tp, sg, docs, web). Механизм работает, но нет ритуала их разбора. Это кандидат на роль администратора.
|
||||
|
||||
## 3. Что сработало (сохранить)
|
||||
|
||||
- **guard-main защитил main**: за 10 дней ни одного коммита в main мимо PR. Все потери — ложные срабатывания, не пропуски.
|
||||
- **Ledger SDD** переживает обрыв контроллера; возобновление без потерь подтверждено повторно.
|
||||
- **Маркировка находок и follow-up issue**: находки не теряются между сессиями, каждая имеет статус.
|
||||
- **`design.md` «Decisions made without discussion»**: десятки решений на срез, прослеживаемы после удаления ledger и чата.
|
||||
- **Финальное ревью ветки на opus** реально находит дефекты (I-1…I-4 в #42, волна C-1/I-1/M-1…M-8 в 3b).
|
||||
- **Golden-тесты** как гарант парсеров: 138/138, 113 снимков, порог `MIN_SNAPSHOTS`.
|
||||
- **Чеклист закрытия** выполняется целиком в большинстве случаев; слабое место — только п. 4 (RT-3).
|
||||
- **Фидбек владельца, записанный в память** (три файла), изменил поведение: сверка с макетом, запрет обхода хуков, режим исполнения.
|
||||
- **Спека как источник истины** при рулингах (F-1 на пилоте, решения №15/№16 в 3a).
|
||||
|
||||
## 4. Бэклог улучшений (по приоритету)
|
||||
|
||||
| # | Что | Находки | Оценка |
|
||||
|---|---|---|---|
|
||||
| B1 | guard-main: решать по цели записи, не по cwd. Игнорировать `N>`/`N>&`; для Edit/Write брать ветку репозитория файла; для Bash — `git -C <dir>` или пути в команде; записи вне git-репозиториев не блокировать; исключения для `git -C … worktree add`, `mkdir -p .wt/`; архивная проверка по путям в команде | RT-1, RT-2, RT-7 | 1 день + тесты |
|
||||
| B2 | Регрессионные тесты хуков на реальных командах из транскриптов (103 случая → фикстуры с ожидаемым вердиктом); запуск из `doctor.sh` | RT-6 | 0,5 дня |
|
||||
| B3 | Закрытие п. 4: явный рецепт в CLAUDE.md и HARNESS.md (push --delete из worktree на ветке фичи) либо allow-правило в проектных настройках по решению владельца; перестать перекладывать на владельца | RT-3 | 0,5 дня |
|
||||
| B4 | Политика архива OpenSpec: либо «всегда отдельным PR» (как фактически), либо workspace-хук учится исключению openspec-only. Привести CLAUDE.md к выбранному | RT-4 | 0,5 дня |
|
||||
| B5 | Роли: skill `/role`, файлы ролей (общие в обвязке, проектные в `.claude/roles/`), агенты с собственной памятью (`jira-admin`, `qa-runner`, `briefing`), подсказка роли по `session_title` | RT-14, RT-11, RT-15 | 2–3 дня |
|
||||
| B6 | Политика памяти: состояние задачи живёт в Gitea (комментарий к issue при закрытии), в память — только уроки, фидбек, знания роли; при «закрывай» файл задачи сворачивается в строку реестра; индекс по ролям; одноразовая чистка 47 файлов | RT-13 | 1 день + чистка |
|
||||
| B7 | Скрипт сборки/сноса многорепозиторного worktree (`wt-new`, `wt-rm`): симлинки, `.bounded`, venv, `node_modules`, (codegraph по флагу); режим «workspace из репозиториев» в `doctor.sh` и `harness-check` | RT-17, RT-19 | 1 день |
|
||||
| B8 | codegraph: вывести из обязательного потока (без `init` на worktree и без правила «вместо grep»), оставить опциональным; решение владельца, нужны ли замеры | RT-12 | 0,5 дня |
|
||||
| B9 | Bounded-чеклист: пункт про спеки при изменении API/схемы; verification-before-completion перед PR | RT-16, RT-18 | 0,5 дня |
|
||||
| B10 | Workspace-хук: искать `git … commit/push` как команду, а не как слово; `reference-old-arm` только для `rm`-целей, не для симлинков | RT-5 | 0,5 дня |
|
||||
| B11 | SDD: раздел «предусловия окружения» в плане (стенд погашен, порты свободны); trailer коммита проверяется ревьюером задачи | RT-8, RT-9 | 0,5 дня |
|
||||
| B12 | Регулярное ретро: skill `/harness-retro` на этих скриптах, метрики: доля ложных блокировок, отказы классификатора, стартовый контекст, невыполненные пункты закрытия, размер памяти; триггер — по команде после среза | все | 1 день |
|
||||
|
||||
Порядок, который предлагается: B1+B2 (убирают главный источник трения и каскад RT-7), затем B5+B6 (роли и память — одно
|
||||
изменение, так как индекс памяти перестраивается под роли), затем B3/B4/B10 (мелкие правила), B12, остальное по мере надобности.
|
||||
|
||||
## 5. Решения, которые нужны от владельца
|
||||
|
||||
1. Архив OpenSpec: отдельный PR всегда (рекомендуется, так уже делается) или исключение в workspace-хуке.
|
||||
2. Удаление серверных веток при закрытии: allow-правило `git push origin --delete` в проектных настройках или рецепт «из worktree».
|
||||
3. codegraph: оставить опциональным (рекомендуется) или провести замер пользы на одном срезе.
|
||||
4. Память: согласие на политику «состояние — в Gitea, в память — уроки и знания роли» и на одноразовую чистку 47 файлов.
|
||||
5. Роли: состав агентов на первом шаге (предложено: `jira-admin`, `qa-runner`, `briefing`; аналитику — файл роли без агента).
|
||||
|
||||
## 6. Что не удалось измерить
|
||||
|
||||
- Время, потерянное на каждую ложную блокировку: транскрипт не содержит длительности исправления, только факт.
|
||||
- Польза codegraph: нет ни одного замера «с ним / без него».
|
||||
- Доля решений в `design.md`, которые владелец потом пересмотрел: нужен разбор архивов OpenSpec, не делался.
|
||||
- Поведение памяти вне git (slug по cwd) не описано в документации Claude Code; проверено только наблюдением.
|
||||
135
docs/retro/scripts/retro_blocks.py
Normal file
135
docs/retro/scripts/retro_blocks.py
Normal file
@ -0,0 +1,135 @@
|
||||
#!/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)")
|
||||
|
||||
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 "BLOCKED" in txt and ("hook" in txt.lower() or "BLOCKED:" in txt[:400]):
|
||||
if name not in ("Bash", "Edit", "Write", "MultiEdit"):
|
||||
continue
|
||||
if "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}")
|
||||
159
docs/retro/scripts/retro_stats.py
Normal file
159
docs/retro/scripts/retro_stats.py
Normal file
@ -0,0 +1,159 @@
|
||||
#!/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}")
|
||||
Loading…
x
Reference in New Issue
Block a user