Merge pull request 'Ретро 11.10.2026, спека ролей и памяти, CLAUDE.md по коду' (#3) from retro/2026-10-11 into main

Reviewed-on: #3
This commit is contained in:
aep 2026-10-12 13:59:04 +03:00
commit c6ce0584fb
6 changed files with 797 additions and 14 deletions

2
.claude/code-paths Normal file
View File

@ -0,0 +1,2 @@
hooks/
skills/

View File

@ -1,32 +1,43 @@
# claude-harness # claude-harness
## Что это ## Что это
{{ОДНИМ АБЗАЦЕМ: что делает система, для кого, главный сценарий.}} Пакет обвязки для разработки с Claude Code: глобальный `CLAUDE.md` с процессом, три блокирующих хука, скрипты подключения проекта и skill диагностики. Для одного разработчика, который ведёт фичи через superpowers, хранит задачи в Gitea и требования в OpenSpec. Главный сценарий: `bash install.sh` ставит глобальную часть в `~/.claude`, `adopt-project.sh` подключает существующий проект, `new-project.sh` создаёт новый; дальше проект живёт по `docs/HARNESS.md`. Репозиторий — источник; установленные копии в `~/.claude` по месту не правятся.
## Стек и ограничения ## Стек и ограничения
- {{Язык, сборка, тесты — например: Python 3.12, uv, pytest.}} - Bash (`set -euo pipefail`) и markdown. JSON из stdin хуков разбирается встроенным `python3`; других зависимостей времени выполнения нет. Внешние CLI (`tea`, `openspec`, `codegraph`) проверяются, но не требуются для установки.
- {{Жёсткие ограничения среды исполнения — без чего код не должен собираться.}} - Хук блокирует вызов только через `exit 2` с сообщением в stderr; `exit 0` — пропустить. Любой сбой разбора входа должен приводить к пропуску, не к блокировке.
- `install.sh` идемпотентен: повторный запуск не ломает `settings.json` и делает бэкапы `CLAUDE.md` и `settings.json` с меткой времени.
- Хуки работают из `~/.claude/hooks/`, пути в `settings.hooks.json` через `$HOME`; `CLAUDE_CONFIG_DIR` переопределяет `~/.claude`.
- macOS и Linux; `python3` должен быть в PATH сессии Claude Code.
## Архитектура ## Архитектура
- `src/core/` (или {{ваш путь}}) — чистая логика без зависимостей от фреймворка/платформы. Покрывается тестами полностью. - `hooks/` — поведение обвязки, единственный «код» с логикой. `guard-main.sh` (PreToolUse на Edit/Write/MultiEdit/Bash): запрет записи на main/master/detached HEAD, исключение для openspec-only коммитов. `require-plan.sh` (PreToolUse на Edit/Write/MultiEdit): правки в каталогах кода только при плане для ветки или файле `.bounded`. `harness-check.sh` (SessionStart): проверка, что проект подключён. `settings.hooks.json` — блок, который `install.sh` сливает в `~/.claude/settings.json`.
- `src/{{adapter}}/` — тонкая обёртка над платформой. Тестируется руками или интеграционно. - `install.sh`, `adopt-project.sh`, `new-project.sh` — установка и бутстрап. `install.sh` копирует `global/`, `hooks/`, `project-template/`, `adopt-project.sh`, `new-project.sh` и `skills/harness-doctor` в `~/.claude/{CLAUDE.md,hooks,harness,skills}`. `adopt-project.sh` работает на ветке `chore/harness-init` и вливает её ff в текущую ветку; это единственная разрешённая запись на main вне PR.
- {{Ключевые интерфейсы/порты между ними, если есть.}} - `global/CLAUDE.md` — глобальные правила процесса, ставятся в `~/.claude/CLAUDE.md`. `project-template/` — `CLAUDE.md`, `CLAUDE.harness-section.md` (секция «OpenSpec и superpowers», по ней `harness-check` узнаёт подключённый проект), `ARCHITECTURE.md`, `gitignore`.
- `skills/harness-doctor/` — skill диагностики: `scripts/doctor.sh` снимает состояние машины и проекта и прогоняет самотест хуков; `SKILL.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/` — планы.
- Связь слоёв: хуки читают JSON вызова из stdin и состояние git проекта; скрипты бутстрапа читают `project-template/`; `doctor.sh` читает установленные копии в `~/.claude`, не репозиторий.
## Команды ## Команды
- `{{npm test / pytest}}` — весь набор тестов. Зелёным считается только весь набор. - `bash install.sh --check` — проверка окружения: бинарники, плагины, конфликты.
- `{{npm run build / ...}}` — сборка. - `bash install.sh` — установка глобальной части, идемпотентно.
- `{{npm run dev / ...}}` — локальный запуск. - `bash skills/harness-doctor/scripts/doctor.sh [проект]` — отчёт и самотест хуков. Только чтение.
- `bash adopt-project.sh <папка> [--no-merge] [--push]` — подключить существующий проект. Из терминала, не из Claude Code.
- `bash new-project.sh <папка> [ssh-url]` — каркас нового проекта.
- `python3 docs/retro/scripts/retro_stats.py <каталог транскриптов>` и `retro_blocks.py` — статистика сессий и блокировок хуков для ретро.
## Тесты ## Тесты
- Golden-тесты: `tests/fixtures/<name>.*` + эталон `<name>.expected.*`. Внешние сервисы в тестах мокаются. - Отдельного набора тестов нет. Единственная автоматическая проверка — самотест хуков в `doctor.sh`: фиктивные вызовы Write/Bash на main и на ветке, `src/` с планом и без, `.bounded`. Прогоняется по установленным копиям, поэтому перед проверкой изменений в `hooks/` сначала `bash install.sh`.
- {{Где лежит тестовое окружение (vault, БД, песочница).}} - Ручная проверка хука: на main в любом репозитории попросить агента создать файл, ожидается `BLOCKED`.
- Регрессионные тесты хуков на реальных командах из транскриптов — бэклог ретро B2, пока не сделаны.
## Первая подсистема ## Текущая работа
{{Что делается первым и почему. Тестовая инфраструктура — Task 1 первого плана.}} Бэклог обвязки — `docs/retro/2026-10-11.md`, раздел 4 (B1–B12), решения владельца — раздел 5. Порядок: B1+B2 (guard-main по цели записи, тесты хуков), затем B5+B6 (роли и память, спека `docs/superpowers/specs/2026-10-11-roles-and-memory-design.md`), затем B3/B4/B10. Находки ретро маркируются `RT-n`.
## OpenSpec и superpowers ## OpenSpec и superpowers
- Вход в любую задачу — superpowers:brainstorming. Skills openspec-propose и openspec-explore сами по себе не запускать. - Вход в любую задачу — superpowers:brainstorming. Skills openspec-propose и openspec-explore сами по себе не запускать.
- Когда brainstorming классифицировал задачу как architectural, структуру change создать через `openspec new change <id>` (или `/opsx:propose <id>`), а содержимое proposal.md, specs/ и design.md написать по итогам brainstorming. tasks.md из OpenSpec не заполнять — задачи живут в плане superpowers. - Когда brainstorming классифицировал задачу как architectural, структуру change создать через `openspec new change <id>` (или `/opsx:propose <id>`), а содержимое proposal.md, specs/ и design.md написать по итогам brainstorming. tasks.md из OpenSpec не заполнять — задачи живут в плане superpowers.
- `/opsx:apply` не использовать: исполнение идёт через writing-plans и SDD/executing-plans. - `/opsx:apply` не использовать: исполнение идёт через writing-plans и SDD/executing-plans.
- `openspec archive <id>` — после merge PR, как часть чеклиста закрытия. - `openspec archive <id>` — после merge PR, как часть чеклиста закрытия.
- Хук require-plan защищает любой каталог `src/` на любой глубине. Если код лежит иначе — перечислить каталоги (префиксы от корня, по одному в строке) в `.claude/code-paths`. - Хук require-plan защищает любой каталог `src/` на любой глубине. Если код лежит иначе — перечислить каталоги (префиксы от корня, по одному в строке) в `.claude/code-paths`. В этом репозитории код — `hooks/` и `skills/`, они перечислены в `.claude/code-paths`.

189
docs/retro/2026-10-11.md Normal file
View File

@ -0,0 +1,189 @@
# Ретроспектива обвязки 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. Решения владельца (11.10.2026)
1. **Архив OpenSpec — всегда отдельным PR.** Исключение openspec-only из guard-main и из CLAUDE.md убирается; правило
«в main только через PR» становится без исключений. (B4 → переформулирован.)
2. **Удаление серверных веток — allow-правило в настройках проекта.** Строку правила владелец добавляет сам; обвязка даёт
готовую строку в `adopt-project.sh` и в отчёте `doctor.sh`. Чеклист закрытия п. 4 остаётся за агентом. (B3.)
3. **codegraph — оставить и замерить.** Один срез с обязательным codegraph и подсчётом вызовов `codegraph_explore` против
Bash/Read в тех же задачах; решение после замера. Метрика входит в `/harness-retro`. (B8 → замер, не вывод из потока.)
4. **Память — политика «состояние задачи в Gitea, в память только уроки, фидбек и знания роли» и одноразовая чистка
47 файлов по ролям.** Закрытые срезы сворачиваются в реестр (строка на задачу: PR, follow-up); чистка на копии, с бэкапом,
дифф показывается до замены. (B6.)
5. Роли: состав агентов на первом шаге решается в спеке ролей (предложено: `jira-admin`, `qa-runner`, `briefing`;
аналитику — файл роли без агента).
## 6. Что не удалось измерить
- Время, потерянное на каждую ложную блокировку: транскрипт не содержит длительности исправления, только факт.
- Польза codegraph: нет ни одного замера «с ним / без него».
- Доля решений в `design.md`, которые владелец потом пересмотрел: нужен разбор архивов OpenSpec, не делался.
- Поведение памяти вне git (slug по cwd) не описано в документации Claude Code; проверено только наблюдением.

View 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}")

View 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}")

View File

@ -0,0 +1,287 @@
# Роли и память: дизайн
Дата: 11.10.2026. Источник: ретроспектива `docs/retro/2026-10-11.md`, находки RT-11, RT-13, RT-14, RT-15, бэклог B5+B6.
Полигон: workspace `awp-ca`. Решение должно подходить и для следующих проектов.
## 1. Цель
Одна сессия Claude Code работает в одной из четырёх ролей: разработчик (`dev`), руководитель/администратор проекта (`admin`),
аналитик требований (`analyst`), QA (`qa`). Сессия получает только контекст, правила и память своей роли, без ручных
просьб «перечитай». Знания роли живут в git-репозитории направления, а не в локальной auto-memory, и переживают смену машины.
Критерии успеха:
- владелец начинает сессию одной фразой или заголовком, роль определяется и объявляется без вопроса, когда она очевидна;
- в сессии admin не загружаются инварианты DBML и процесс superpowers→Gitea→OpenSpec; в сессии dev не загружается Jira;
- ни одно правило из нынешних `~/.claude/CLAUDE.md` и `awp-ca/CLAUDE.md` не потеряно: у каждого абзаца есть новый адрес;
- глобальный CLAUDE.md ≤ 3 КБ, проектный ≤ 5 КБ, индекс auto-memory ≤ 3 КБ;
- стартовый контекст не-dev сессий меньше текущей медианы 56 К токенов (замер скриптом ретро);
- хуки guard-main, require-plan, harness-check не меняются.
## 2. Модель workspace
Все сессии запускаются из корня workspace (не git). Рядом лежат репозитории направлений.
```
<project>/ workspace, не git, cwd всех сессий
CLAUDE.md общий минимум: что за проект, таблица репозиториев с ролью-владельцем, источники истины, «роли → /role»
.claude/
settings.json как сейчас (хуки workspace, разрешения)
roles/<r>.md проектный слой ролей (dev — файл; admin/analyst/qa — симлинки в репозитории направлений)
agents/<name>.md симлинки на агентов из репозиториев направлений
agent-memory/<name>/ симлинки на память агентов в репозиториях направлений
<code repos>/ роль dev
docs/ витрина: контекст, модель, контракты, гайды, инструкции. Читают все, пишут через PR
admin/ → arm-ka-admin кухня admin: Jira, Гант, спринты, документы заказчику, notes/, .claude/
qa/ → arm-ka-qa кухня qa: сценарии, прогоны, скриншоты, находки, notes/, .claude/
analysis/ → arm-ka-analysis кухня analyst: черновики OQ, протоколы, сверки ТЗ с кодом, notes/, .claude/
```
Правило: **docs/ публикует, репозиторий направления работает.** У каждого раздела docs/ есть роль-владелец:
`context/`, `model/` — analyst; `dev/`, `contracts/` — dev; `user/`, `dev/qa-guide.md` — qa; `source/` — никто (экспорт).
Рабочий материал (журналы прогонов, скриншоты, выгрузки Jira, черновики решений) в витрину не попадает или попадает после обработки.
Репозиторий направления содержит:
```
<dir>/
CLAUDE.md что это за репозиторий, 5–10 строк (загружается лениво при чтении файлов из него)
.claude/role имя роли по умолчанию (одна строка) — для будущего режима запуска из репозитория
.claude/roles/<r>.md проектный слой роли
.claude/agents/<name>.md агенты роли
.claude/agent-memory/<name>/MEMORY.md память агентов (коммитится)
notes/ уроки, решения владельца, рецепты роли (markdown, коммитится)
<рабочие каталоги>
```
Симлинки из `<workspace>/.claude/` в репозитории направлений создаёт скрипт `link-roles.sh` (раздел 8), проверяет `doctor.sh`.
Claude Code находит агентов и skills только в `<cwd>/.claude/`, поэтому без ссылок конфигурация направлений из корня не видна.
## 3. Слои ролей
Два слоя, оба в git:
| Слой | Где в источнике | Куда ставится | Что содержит |
|---|---|---|---|
| Общий | `claude-harness/roles/<r>.md` | `~/.claude/roles/<r>.md` (install.sh) | процесс роли, не зависящий от проекта |
| Проектный | `<repo направления>/.claude/roles/<r>.md`, для dev — `<workspace>/.claude/roles/dev.md` | симлинк в `<workspace>/.claude/roles/` | пути, инструменты, люди, ограничения проекта |
При конфликте побеждает проектный слой. Общий слой `roles/dev.md` получает целиком нынешний процесс разработки из
`global/CLAUDE.md` (маршрутизация, исполнение, завершение, маркировка, стиль). Проектный `roles/dev.md` в awp-ca получает
из `awp-ca/CLAUDE.md` инварианты, стек, зеркальный worktree `.wt/`, DBML, `05-domain-rules`, стенд, «чего не делать».
### 3.1 Формат файла роли
Одинаков для обоих слоёв. Четыре раздела, заголовки фиксированы, чтобы `/role` мог их склеить:
```markdown
# Роль: admin — руководитель/администратор проекта
Эта роль заменяет предыдущую роль сессии целиком.
## Процесс
- какие skills и чеклисты действуют; что по умолчанию bounded; где трекер задач этой роли
## Читать на старте
- пути (относительно workspace) и команды; строки с префиксом `brief:` уходят агенту briefing, остальные читаются в контекст
## Не делать
- явные запреты роли
## Делегировать
- агент → для чего
```
### 3.2 Содержание общих файлов ролей (обвязка)
- **dev** — нынешний глобальный CLAUDE.md без изменений по смыслу, плюс уроки из ретро, не зависящие от проекта:
не `cd` в основной checkout, пока работает фоновый контроллер; абсолютные пути worktree в планах; trailer коммита
проверяет ревьюер задачи; архив OpenSpec всегда отдельным PR (решение владельца 11.10.2026); пункт 4 чеклиста закрытия —
через allow-правило проекта. Bounded-чеклист дополняется: «если меняется поведение API или схемы, спека обновляется в том же PR».
- **admin** — задачи ведутся в трекере заказчика (Jira) или в репозитории направления, Gitea-issue и OpenSpec не заводятся;
brainstorming остаётся входом, задачи по умолчанию bounded; код только в репозитории направления (скрипты, TDD);
тексты для внешних людей самодостаточны (без внутренних маркеров, номеров PR и путей); артефакты для передачи — в `deliverables/`.
- **analyst** — работа с требованиями: спеки, открытые вопросы, решения владельца, сверка ТЗ с кодом; код не пишется;
публикация в docs/ через PR; противоречия документов не решаются молча, а фиксируются вопросом.
- **qa** — стенд, прогоны сценариев, сверка каждого экрана с макетом, гайды и инструкции; код не меняется;
находки — списком со скриншотами, issue в кодовый репозиторий заводит dev по решению владельца.
### 3.3 Проектные файлы ролей awp-ca
- `dev` (workspace): репозитории и их CLAUDE.md, зеркальный worktree `.wt/<задача>/` с симлинками, `.bounded`, venv через
`uv venv --python 3.11`, стенд `:8000/:3000`, DBML v5.18 и `05-domain-rules` как источники истины, инварианты (первичка не
мутируется, производные не хранятся, VERIFIED read-only, единственный писатель arm-core, права — данные, один файл = N
документов, парсеры не по координатам, деньги numeric, append-only аудит), «чего не делать», синхронизация `arm-core/web`
скриптом `sync-web.sh`, codegraph обязателен на период замера (решение владельца 11.10.2026).
- `admin` (в `admin/`): `admin/README.md`, пакет `jira-sync/` и его README, Jira AURORA (доска 4216, правила текстов задач,
переходы статусов), Гант (`source/*.mpp`, читать mpxj, Ган не трогать), просмотрщик `sprint-2-links.html`, команда и зоны
ответственности, текущие операционные ограничения: `apply` в реальную Jira выполняет только владелец на виртуалке
(ограничение снимается вместе с мостом из `jira-sync/next-session-bridge.md`). `brief:` — `admin/notes/`, последний `jira-sync/runs/<дата>/`.
- `analyst` (в `analysis/`): `docs/context/05-domain-rules.md`, `07-open-questions.md`, `04-data-model-guide.md`, DBML,
`docs/source/tz/` (имена v4, алгоритмы рабочие), `08-source-docs-index.md`; как оформлять OQ и решения; `brief:` — `analysis/notes/decisions.md`, открытые OQ.
- `qa` (в `qa/`): `docs/dev/qa-guide.md` (§8 чек-лист UI), стенд и его ловушки, Playwright (окно 1600×1000, макет через
`http.server`, запасной runner), `docs/mockup/screens/*.png`, `docs/user/user-guide.md` и рецепт скриншотов и docx,
контрольные цифры прогонов; `brief:` — `qa/notes/`, последний `qa/runs/<дата>/`.
## 4. Выбор роли
Три входа, от автоматического к ручному. Запуск всегда из корня workspace, git-корень для определения роли не используется.
1. **Заголовок сессии.** `claude -n "admin …"` (алиасы `cc-admin`, `cc-dev`, `cc-qa`, `cc-analyst` в профиле shell —
рекомендация в README, не часть install.sh). Хук `hooks/role-hint.sh` на SessionStart (все источники: startup, resume,
clear, compact) читает `session_title`; если он начинается с имени роли, печатает в stdout одну строку:
`[claude-harness] Роль сессии: admin. Вызови /role admin до первого ответа.` Без заголовка молчит. Ничего не блокирует.
2. **Вывод из первого сообщения.** Правило в глобальном CLAUDE.md: если роль очевидна (Jira, Гант, спринт → admin; стенд,
прогон, скриншот, макет → qa; ТЗ, OQ, спека, методика → analyst; срез, issue, PR, тест, миграция → dev), сессия объявляет
её одной строкой «Роль: admin; если не так, скажи» и вызывает `/role`. Если неочевидна, один вопрос с четырьмя вариантами.
3. **Явно.** `/role <r>` в любой момент. Новая роль заменяет предыдущую, первая строка файла роли это говорит.
## 5. Skill `/role`
Источник: `claude-harness/skills/role/SKILL.md`, ставится в `~/.claude/skills/role/`. Frontmatter: `name: role`,
`description`, `argument-hint: dev|admin|analyst|qa`, `user-invocable: true`. Шаги:
1. Прочитать `~/.claude/roles/<r>.md` и `<workspace>/.claude/roles/<r>.md` (если проектного нет — сказать об этом одной строкой и работать по общему).
2. Прочитать раздел роли из индекса auto-memory (`## <Роль>`) и перечисленные в нём файлы.
3. Собрать строки `brief:` из раздела «Читать на старте» обоих слоёв и, если они есть, запустить агента `briefing`
в фоне (`Agent`, `subagent_type: briefing`, `run_in_background`), передав роль и список. Остальные строки прочитать в контекст.
4. Ответить пятью строками: роль; действующий процесс; что прочитано; чего в этой роли не делать; что ждёт решения владельца
(из памяти роли). Когда придёт выжимка briefing, добавить её одним сообщением без пересказа.
`/role` без аргумента печатает текущую роль и список доступных.
## 6. Агенты
Все агенты — markdown с frontmatter Claude Code (`name`, `description`, `model`, `tools`, `memory`). Модель указывается явно.
### 6.1 `briefing` (общий, обвязка)
`claude-harness/agents/briefing.md` → `~/.claude/agents/briefing.md`. `model: sonnet`, `tools: Read, Glob, Grep, Bash`,
без `memory`. Промпт: получает роль и список путей/команд; команды выполняет только читающие (`tea issues ls`, `openspec list`,
`git log`, `ls`); возвращает выжимку до 60 строк в фиксированной форме: **Активные задачи** / **Решения за последние дни** /
**Открытые вопросы** / **Ловушки**. Не предлагает действий, не редактирует файлы.
### 6.2 `jira-admin` (в `admin/.claude/agents/`)
`model: sonnet`, `memory: project`, `tools: Read, Write, Edit, Bash, Glob, Grep`. Промпт содержит только инварианты,
не зависящие от инструмента:
- CSV в `admin/jira-sync/` — источник истины и единственное место ключей созданных задач; ключи пишутся обратно сразу;
- задания декларативные (что сделать), не произвольные команды; пароль Jira нигде, кроме `config.json` виртуалки;
- тексты задач по правилам README пакета (самодостаточные, структура h3 Контекст / Что сделать / Критерии готовности / Источники);
- инструментарий берётся из `admin/jira-sync/README.md` и доступных MCP-инструментов; что можно выполнять против реальной Jira — из проектного файла роли admin.
Задачи: `discover`/`plan` в сухом режиме, перегенерация CSV, сверка Ганта с выгрузкой, черновики описаний, разбор спринта по CSV.
Память: customfield'ы, ловушки REST (направление `issueLink`), люди и зоны, история актуализаций.
### 6.3 `qa-runner` (в `qa/.claude/agents/`)
`model: sonnet`, `memory: project`, инструменты: Read, Bash, Glob, Grep и Playwright MCP (`mcp__plugin_playwright_playwright__*`),
без Write/Edit в репозиториях кода (запись только в `qa/runs/`). Задачи: прогон сценария на локальном стенде, сверка каждого экрана
с PNG макета при окне 1600×1000, список расхождений со скриншотами в `qa/runs/<дата>/`. Не меняет код; находки отдаёт списком.
Память: ловушки стенда, порты, токены (`sessionStorage['armka.token']`), запасной runner playwright-core, контрольные цифры.
### 6.4 Что не меняется
SDD-контроллер и implementer-ы остаются описанием в `roles/dev.md` (superpowers запускает их сам). Аналитик без агента.
### 6.5 Память агентов через симлинк
`memory: project` пишет в `<cwd-проект>/.claude/agent-memory/<name>/`. Чтобы память оказалась в git репозитория направления,
`<workspace>/.claude/agent-memory/<name>` — симлинк в `<dir>/.claude/agent-memory/<name>`. Проверяется первой (раздел 9).
Запасной вариант: поле `memory` не используется, промпт агента велит читать `<dir>/notes/agent-memory.md` при старте и дописывать в конце.
## 7. Память
### 7.1 Политика (в глобальный CLAUDE.md, три строки)
- Состояние задачи живёт в трекере (Gitea issue, Jira), не в памяти.
- В репозиторий направления (`<dir>/notes/`, память агента) пишутся уроки, решения владельца, рецепты. Коммит в той же ветке, что и работа.
- В локальную auto-memory — только ловушки этой машины и обвязки и индекс со ссылками на `notes/` направлений.
Чеклист закрытия, новый пункт 8: файл auto-memory задачи, если был, сворачивается в строку `registry-closed.md`
(issue, PR, follow-up, дата); уроки из него переносятся в `notes/` направления.
### 7.2 Индекс auto-memory после чистки
```markdown
## Общее — ловушки машины, обвязка, фидбек владельца по процессу
## Разработка — ссылка на .claude/roles/dev.md и docs/dev/, активные уроки
## Администрирование — ссылка на admin/notes/, память jira-admin
## Аналитика — ссылка на analysis/notes/decisions.md
## QA — ссылка на qa/notes/, память qa-runner
## Закрытое — registry-closed.md
```
### 7.3 Одноразовая чистка 47 файлов awp-ca
На копии памяти в песочнице; дифф показывается владельцу до замены; оригинал сохраняется как `memory.bak-<дата>/`.
| Куда | Что |
|---|---|
| `registry-closed.md` (auto-memory) | 30 файлов «ЗАКРЫТ»: одна строка на задачу |
| `admin/notes/jira.md` + память `jira-admin` | `jira-backlog-actualization`, трудозатраты из `slice-0-proposals-state`, рецепт Word из `user-guide-85-state` |
| `qa/notes/` + память `qa-runner` | `qa-local-stand`, `q1-2025-run`, `pdfjs-legacy-yandex`, рецепт скриншотов, `feedback-compare-with-mockup` (как правило роли qa) |
| `analysis/notes/decisions.md` | решения владельца по OQ из `oq-decisions-62`, `slice-1/2c/3a/3b/4-state`, `tech-debt-slice-1`, `osv-inn-mapping-rework`, `auto-confirm-64`, `statement-template-lines-state` |
| `<workspace>/.claude/roles/dev.md` или `docs/dev/` | `workspace-hooks-and-worktrees` (уроки), `nats-platform-fileworker`, `arm-web-repo`, `sync-web-script-state` |
| `claude-harness/roles/dev.md` | уроки без привязки к проекту (cwd и контроллер, пути в планах, trailer) |
| остаются в auto-memory | `feedback-no-harness-bypass`, `feedback-execution-mode-from-claude-md`, `harness-retro-and-roles`, `jetstream-check-later` |
`admin/jira-sync/next-session-bridge.md` после переноса ссылается на `admin/notes/jira.md` вместо памяти `jira-backlog-actualization`.
## 8. Изменения в обвязке (claude-harness)
| Файл | Изменение |
|---|---|
| `global/CLAUDE.md` | сжать до ≤ 3 КБ: язык, хуки, worktree, маркировка, политика памяти (7.1), правило выбора роли (раздел 4), «процесс роли — в /role» |
| `roles/{dev,admin,analyst,qa}.md` | новые, формат 3.1 |
| `skills/role/SKILL.md` | новый, раздел 5 |
| `agents/briefing.md` | новый, 6.1 |
| `hooks/role-hint.sh` + `hooks/settings.hooks.json` | новый SessionStart-хук; слияние блока в install.sh как у harness-check |
| `link-roles.sh` | новый: по таблице репозиториев в `CLAUDE.md` workspace (или по аргументам) создаёт симлинки `.claude/roles`, `.claude/agents`, `.claude/agent-memory`; идемпотентно; печатает, что сделано |
| `install.sh` | ставит `roles/`, `skills/role`, `agents/briefing.md`, `hooks/role-hint.sh`, `link-roles.sh` в `~/.claude/harness/`; `--check` проверяет их наличие |
| `project-template/` | `.claude/roles/*.md` заготовки; `role-repo/` шаблон репозитория направления (CLAUDE.md, `.claude/role`, `notes/README.md`) |
| `adopt-project.sh` / `new-project.sh` | режим `--workspace`: создать `CLAUDE.md` workspace, `.claude/roles/`, вызвать `link-roles.sh`; существующий режим не меняется |
| `skills/harness-doctor/scripts/doctor.sh` | проверки: симлинки ролей целы, `role-hint.sh` отвечает на тестовый JSON, размеры CLAUDE.md и индекса памяти |
| `docs/HARNESS.md`, `README.md`, `CHANGELOG.md` | раздел «Роли», версия 0.3.0 |
Хуки `guard-main.sh`, `require-plan.sh`, `harness-check.sh` не меняются (правки хуков — отдельная работа B1+B2).
## 9. Песочница, проверка, перенос
Песочница `~/projects/awp-ca-roles/`: симлинки на все репозитории awp-ca, копии `CLAUDE.md`, `.claude/`, копия памяти
(своя auto-memory из-за другого cwd). Репозитории `admin/`, `qa/`, `analysis/` создаются настоящими git-репозиториями
в `awp-ca/` (git init не трогает файлы; репозитории в Gitea заводятся по команде владельца). Ветка обвязки ставится
в песочницу не через `~/.claude/`, а через `CLAUDE_CONFIG_DIR` только если это понадобится; по умолчанию общий слой
проверяется копированием файлов ролей в `.claude/` песочницы, а `install.sh` выполняется один раз при переносе.
Проверки (каждая — задача плана с ожидаемым результатом):
1. Симлинки агентов и памяти: агент виден в списке Agent, запись в память попадает в `admin/.claude/agent-memory/`. Иначе — запасной вариант 6.5.
2. `role-hint.sh`: JSON с `session_title: "admin …"` даёт строку подсказки; без заголовка — пустой вывод; тест в `doctor.sh`.
3. Четыре сессии (`claude -n admin|qa|analyst|dev`): `/context` показывает файлы своей роли и не показывает чужие; стартовый контекст не-dev сессий меньше 56 К (скрипт `docs/retro/scripts/retro_blocks.py` по транскрипту песочницы).
4. `/role admin` в dev-сессии переключает правила; `briefing` возвращает выжимку.
5. Размеры: глобальный CLAUDE.md ≤ 3 КБ, проектный ≤ 5 КБ, индекс ≤ 3 КБ. Сверка «каждый абзац старых CLAUDE.md имеет адрес» — таблицей в PR.
6. Чистка памяти: дифф на копии, таблица 7.3 выполнена, `next-session-bridge.md` обновлён.
Перенос по команде владельца: дифф `~/.claude/CLAUDE.md` и памяти с состоянием на момент форка (11.10.2026) → `install.sh`
из ветки обвязки → копирование `.claude/` и `CLAUDE.md` из песочницы в `awp-ca/` → замена памяти с бэкапом → удаление песочницы.
## 10. Вне объёма
- Правки хуков guard-main/workspace (B1, B2, B10) — отдельная ветка после ролей.
- Мост jira-sync (`admin/jira-sync/next-session-bridge.md`) — brainstorming в роли admin после переноса.
- Skill `/harness-retro` (B12) — третий подпроект; сюда входит только размещение скриптов ретро в `docs/retro/scripts/`.
- Запуск сессий из репозитория направления (`.claude/role` по git-корню) — файл создаётся, механизм не реализуется.
## 11. Открытые вопросы
- Работает ли `memory: project` через симлинк каталога (проверка 1). Пока не проверено, в плане первой задачей.
- Имена репозиториев в Gitea: предложено `RSHB/arm-ka-admin`, `RSHB/arm-ka-qa`, `RSHB/arm-ka-analysis`; подтверждает владелец при создании.
- Алиасы `cc-<роль>`: в профиле shell владельца, обвязка только документирует.
## Decisions made without discussion
- 11.10.2026. Файлы ролей общего слоя ставятся в `~/.claude/roles/`, а не внутрь skill `role`. Обоснование: проектный слой
лежит в `.claude/roles/`, одинаковый путь в обоих слоях проще объяснить и проверить в doctor. Цена ошибки: перенос каталога.
- 11.10.2026. Агенты направлений объявляются в репозитории направления и подключаются симлинками, а не копируются в workspace.
Обоснование: один источник в git, видимость из корня. Цена ошибки: если симлинки не работают, копирование скриптом `link-roles.sh`.
- 11.10.2026. `briefing` запускается в фоне, а не блокирует `/role`. Обоснование: владелец может начать говорить сразу.
Цена ошибки: выжимка приходит после первого ответа; если это мешает, флаг `--wait`.
- 11.10.2026. Спека лежит в `docs/superpowers/specs/` репозитория обвязки, а не в `openspec/`: обвязка к самой себе не
подключена, OpenSpec в ней нет. Вопрос «подключать ли обвязку к обвязке» открыт, владельцу не задавался. Цена ошибки: перенос файла.