Сигнал «метка, вероятно, занижена» жил в review-basics — в единственном месте. А basics с меткой small не запускается, если у проекта нет своих тем: значит на типичном проекте задача с меткой small шла без рантайм-проверки того, что метка выбрана верно. Дыра появилась вместе с удешевлением small и попала в самую вероятную точку ошибки: занижают туда, где дешевле, а цена занижения там же и выросла — три темы ядра смотрятся только против записанных инвариантов. Сигнал перешёл в review-code, и он подходит по построению: идёт при любой метке, видит дифф целиком, а на small уже читает инварианты, то есть держит весь материал, из которого сигнал выводится. Признаков четыре, и один весит больше прочих — изменение, которое не откатывается обратной правкой, при метке small это прямой промах отрицательного теста. У basics сигнал остался вторым, подтверждающим: он смотрит оптикой тем и видит то, чего не видно из кода как кода, — что вопросов, отложенных до large, накопилось слишком много. Триаж теперь обязан сказать и когда сигнала нет: «корректор отработал, возражений нет» и «корректор не запускался» по молчанию неразличимы. У small появилась доля, и она сформулирована сравнением, а не порогом: small не должен обгонять medium, ориентир — до трети задач. Проверка нужна именно теперь. Пока quick и standard совпадали составом, дрейф между ними не стоил ничего, и её не было; сейчас он стоит трёх тем ядра. У дрейфа вниз есть стимул, и он назван прямо: метку выбирает не автор, но по описанию, написанному автором — занижённое описание даёт занижённую метку без чьего-либо умысла. Размер теперь считается по корпусу из пяти источников. Разметчик читал proposal.md и tasks.md, но design.md не открывал вовсе, а метод был описан одной фразой «размер считается по дельта-спекам». Дельты описывают заказанное поведение и молчат об объёме работы: шесть шагов в двух узлах видны в tasks.md, а факт, что форму решения выбирали из нескольких, — только в design.md. Каждый источник получил свою строку по каждой оси, и каждая цифра обоснования обязана быть привязана к источнику поимённо; «изменение выглядит средним» обоснованием больше не считается. Отсюда два правила, которых не было. Расхождение источников по объёму разрешается в пользу большего — и это не «спорное решается вниз»: то правило разрешает ничью при равных данных, а здесь один источник просто видел больше. Само расхождение при этом идёт доводом за незнакомое: если о задаче написано так, что источники не сходятся в объёме, форму решения по ней не знают. Отсутствие design.md у нетривиальной задачи читается так же — «форму знали заранее» ничем не подтверждено. Заодно две грамматические опечатки от вчерашнего переименования в SKILL.md. Решение — 45. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
av-dev-skills
Личный маркетплейс плагинов для разработки. Процесс имени не имеет — он и есть
av-dev.
Что решено и почему — DECISIONS.md. Что осталось сделать — TODO.md и REMAINING.md. Как процесс дошёл до текущей формы — HISTORY.md.
Плагины
- av-dev-pm — управление продуктом. Владеет всем
docs/.init— новый проект: интервью по свободному описанию замысла → первичная документация;canon— привести проект к канону документов:check/adopt/upgrade, плюс скриптdocs.py. Там же живёт язык проектных текстов — информационный стиль, англицизмы, жаргон. Смысловую часть, которой скрипт не видит, судят два агента:doc-consistency(документы между собой и с openspec) иdoc-code-drift(документы против кода);docs— содержимое канона по ходу разработки: ADR из архивногоdesign.md, промоут конвенций, запись в разведку и журнал ревью, чистка архитектуры;tasks— задачи и цели каталогом markdown-файлов, у каждой записи тип (goal,feature,fix,chore,research), и тип задаёт её схему; вычитывают их два отдельных прохода:task-form(форма записи) иdoc-wording(язык);session— ритуал между спринтами и ведение спринта.
- av-dev-pipeline — исполнение. Требует OpenSpec.
task-pipeline— задача через полный цикл SDD, от постановки до коммита;task-batch— несколько задач разом, каждая в своём worktree;review-pipeline— конвейер ревью по темам: документ проекта либо заводит тему проверки, либо питает чужую тему источником, либо процессный и в ревью не читается вовсе. Разметка идёт один раз на задачу, сразу послеpropose: агентreview-scopeмеряет изменение по двум осям — размер и сложность — и берёт метку как максимум по ним. Одна метка правит обе стадии ревью: дизайна (small— только сверка спек;medium— плюс рубрика;large— плюс архитектурный проход) и кода (small— гейт, спеки, код, триаж;medium— плюс приёмник тем;large— плюс доказательство: запуск, замер, построенный путь, 5–10% задач). Десять агентов-проходов.
- av-dev-git —
commit: сообщения в личном стиле.
Соглашение об именах: имя плагина длинное с префиксом av-dev-, имена
скилов внутри — короткие. Вызов выходит вида /av-dev-<плагин>:<скилл>.
Кто кого зовёт (стрелка — вызов через пространство имён, не импорт):
flowchart TB
subgraph pipe["av-dev-pipeline — исполнение, требует OpenSpec"]
direction LR
batch["task-batch"] --> tp["task-pipeline"]
tp --> rp["review-pipeline<br/>10 агентов-проходов"]
batch --> rp
end
subgraph pm["av-dev-pm — управление продуктом, владеет docs/"]
direction LR
init["init"] --> tasks["tasks"]
canon["canon"] --> tasks
session["session"] --> tasks
docs["docs"]
end
opsx["opsx:* — внешний плагин:<br/>explore, propose, apply, archive"]
git["av-dev-git: commit"]
tp --> opsx
tp --> git
tp --> docs
tp --> tasks
Зависимость односторонняя: av-dev-pipeline знает про av-dev-pm, обратно —
нет. Управление продуктом работает в проекте без конвейера; конвейер без
канона деградирует поразрядно и говорит об этом строкой.
Канон документов проекта
Все проекты приводятся к одной раскладке — так проще ориентироваться, когда
проектов много, и рядом OpenSpec тоже держит строгую структуру. CLAUDE.md плюс
docs/ (паспорт, архитектура, база, безопасность, конвенции, разведка, ADR,
ревью, задачи) и openspec/ — раскладка целиком, роли документов и правило
единственного дома живут одним домом:
canon.md. Здесь она не
пересказывается: копия перечня путей уже расходилась с домом, и как раз в
обязательных — в ней не хватало путей, чьё отсутствие docs.py check считает
нарушением.
Документы канона делятся на три категории, и разрез проверяемый: можно ли по
документу сказать «в этом изменении сделано не так». Тема — да, прямо
(conventions, security, architecture и любой свой документ проекта; список
тем открытый — завёл документ, завёл направление проверки). Источник темы —
нет, но он задаёт границу для чужой темы (passport, database, CLAUDE.md,
openspec/specs/). Процессный документ — нет, он про то, как мы работаем
(tasks/, review.*, adr.*, research.*); ревью изменения по нему не судит.
Форма дома — файл или каталог, на выбор проекта.
Отдельного файла-брифа при этом нет — проходы читают документы напрямую; карта «тема → её дом → что оттуда берётся» — project-facts.md.
Прийти в старый проект и перевести его на канон — /av-dev-pm:canon. Канон
версионируется, и проекты повышаются по журналу
версий.
Подключение
Плагин подключается на уровне проекта, чтобы был активен у всех, кто открывает репозиторий. Из терминала, в каталоге проекта:
cd /path/to/project
# маркетплейс: один раз на проект. --scope project кладёт его
# в extraKnownMarketplaces этого репозитория (см. ниже), без флага — в user
claude plugin marketplace add https://git.vakhrushev.me/av/dev-skills.git --scope project
# плагины: scope обязателен, умолчание у команды — user, а нам нужен project
claude plugin install av-dev-pm@av-dev-skills --scope project
claude plugin install av-dev-pipeline@av-dev-skills --scope project
claude plugin install av-dev-git@av-dev-skills --scope project
Те же команды изнутри Claude Code — со слешем: /plugin marketplace add …,
/plugin install … --scope project. Обе формы дописывают в
.claude/settings.json проекта то, что можно внести и руками:
{
"extraKnownMarketplaces": {
"av-dev-skills": {
"source": { "source": "git", "url": "https://git.vakhrushev.me/av/dev-skills.git" }
}
},
"enabledPlugins": {
"av-dev-pm@av-dev-skills": true,
"av-dev-pipeline@av-dev-skills": true,
"av-dev-git@av-dev-skills": true
}
}
При установке в проект, где лежали проектные копии скиллов и агентов
(.claude/skills/ — голые task-pipeline, review-pipeline, task-batch
и с префиксом проекта <проект>-task-pipeline,
.claude/agents/<проект>-review-*.md) — снеси их. Две копии одного скилла
расходятся, и побеждает та, что короче названа.
Обновление
Обновление — два шага, и первого мало. marketplace update тянет git-клон
маркетплейса, но снимки плагинов лежат отдельно, в
~/.claude/plugins/cache/av-dev-skills/<плагин>/<версия>/, и обновляются только
командой plugin update. Один шаг без второго выглядит как «обновил, а ничего не
изменилось» — так и было в первый раз.
# 0. отправить свои коммиты: до клона доезжает только то, что на origin
git -C /path/to/dev-skills push origin master
# 1. клон маркетплейса
claude plugin marketplace update av-dev-skills
# 2. снимки плагинов — из каталога проекта, где они установлены
cd /path/to/project
claude plugin update av-dev-pm@av-dev-skills --scope project
claude plugin update av-dev-pipeline@av-dev-skills --scope project
claude plugin update av-dev-git@av-dev-skills --scope project
cd в проект обязателен, и это не педантизм. Команда правит запись реестра, а
записей столько, во скольких проектах плагин установлен; за вызов двигается
одна. Запущенная не из проекта, она обновит какую-то из них — наблюдалось:
av-dev-git стоял в шести проектах, один вызов поднял версию ровно в одном, и не
в том, из которого звали. Проекты обновляются поштучно.
Что стоит и какой версии — одной командой:
python3 - <<'EOF'
import json, pathlib
d = json.loads(pathlib.Path.home().joinpath(".claude/plugins/installed_plugins.json").read_text())
for name, entries in sorted(d["plugins"].items()):
if "av-dev" not in name:
continue
for e in entries:
print(f"{e['version']:14} {name:30} {e.get('projectPath', e.get('scope', ''))}")
EOF
Версия — первые 12 знаков хеша коммита этого репозитория, так что сверяется
глазами с git rev-parse HEAD | cut -c1-12. Команда идемпотентна: на уже свежем
плагине скажет already at the latest version.
Изменения применяются после перезапуска Claude Code — работающая сессия держит скиллы в контексте и про новый снимок не знает.
Снятие
Действие, обратное подключению.
cd /path/to/project
claude plugin uninstall <плагин>@av-dev-skills --scope project
Команда правит два места: убирает строку из enabledPlugins в
.claude/settings.json проекта и запись из реестра
~/.claude/plugins/installed_plugins.json. Снимок в
~/.claude/plugins/cache/av-dev-skills/<плагин>/<версия>/ не трогает — он
общий для всех проектов. Маркетплейс тоже остаётся: extraKnownMarketplaces
нужен остальным плагинам.
Удалять из маркетплейса можно и до снятия с проектов. uninstall идёт по
реестру, а не по marketplace.json, и снимает плагин, записи о котором в
манифесте уже нет. Проверено на av-dev-backlog: удалён из маркетплейса,
снят с jellybit после — команда отработала штатно.
--scope project обязателен по той же причине, что и при установке: умолчание у
команды — user. cd в проект обязателен, но здесь ошибка слышна — вызванная не
оттуда, команда откажется словами is not installed in project scope, а не
снимет плагин наугад, как это делает plugin update.
Убрать строку из settings.json руками — половина дела: запись в реестре
переживает такую правку, и в инвентаризации проект продолжает числиться. Лечится
той же командой из каталога проекта — она отработает и когда в settings.json
уже пусто.
Снялось или нет — видно инвентаризацией из раздела Обновление. Применяется после перезапуска Claude Code, как и обновление.
Структура репозитория
.claude-plugin/marketplace.json манифест маркетплейса
<plugin>/.claude-plugin/plugin.json манифест плагина
<plugin>/skills/<skill>/SKILL.md скилы (авто-обнаружение)
<plugin>/skills/<skill>/references/ что читается по ссылке из скилла
<plugin>/skills/<skill>/scripts/ tasks.py, docs.py
<plugin>/agents/ charter'ы сабагентов
scripts/ проверки репозитория: копии, диаграммы, фронтматтеры
pyproject.toml линтеры скриптов, только для этого репозитория
lefthook.yml гейт коммита: проверки документов
Проверка скриптов
tasks.py и docs.py запускаются где угодно голым python3 3.12 без
установки чего-либо — они лежат рядом со скиллами и работают в любом проекте.
pyproject.toml в корне не меняет этого: он живёт только здесь и держит
линтеры, а не зависимости скриптов.
uv sync # ставит ruff и pyrefly в .venv, версии прибиты точно
uv run ruff check . # правила; --fix для безопасных починок
uv run pyrefly check # типы
Ноль внешних зависимостей охраняется двумя способами: banned-api у ruff ловит
частые соблазны по имени, а pyrefly видит окружение, где нет ничего кроме
линтеров, и любой сторонний импорт у него не разрешается. Список запретов —
не перечень мира, настоящий страж второй.
Проверка фронтматтеров
Фронтматтер читает не человек, а загрузчик: по name он разрешает вызов, по
description решает, звать ли скилл вообще. Ошибка здесь не выглядит
ошибкой — тем же способом, что и в диаграммах.
uv run python scripts/frontmatter.py # 0 в порядке, 1 расхождение, 3 не тот каталог
Ловится три класса:
- двоеточие с пробелом в описании без кавычек. Для YAML
:внутри простого скаляра начинает вложенное отображение, так что «конвейер ревью: гейт, сверка…» — это не текст с двоеточием, а синтаксическая ошибка. Так было написано часть описаний плагинов, и читались они правильно — замер и разбор в DECISIONS.md, решение III; name, разошедшееся с именем каталога скилла или файла charter'а. Вызов разрешается по каталогу, а сообщение о промахе говорит «нет такого скилла», а не «имя не то»;- цвет charter'а, не отвечающий его модели. Цвет кодирует модель, а не роль прохода — раскладка живёт в review-pipeline/SKILL.md, разделе «Модель по проходу», здесь только её механизация. Держаться вниманием правило не может: цвет ставится один раз при заведении charter'а, а модель потом меняется калибровкой.
Проверка копий правил
«Один факт — один дом» держалось вниманием и трижды не удержалось. Копии всё же нужны: скелеты канона уезжают в репозиторий проекта и обязаны там что-то говорить. Значит копия допустима, но дословная и помеченная:
uv run python scripts/copies.py # 0 сошлось, 1 расхождение, 2 разметка, 3 не тот каталог
Разметка — HTML-комментарии, невидимые в отрендеренном markdown:
<!-- дом: <id> --> …текст… <!-- /дом: <id> -->
<!-- копия: <id> из <путь к дому> --> …тот же текст… <!-- /копия: <id> -->
Идентификатор — буквы, цифры и дефис, и он повторяется в закрывающем маркере.
Строгость нужна ровно затем, чтобы этот абзац сам не объявил дом: <id> под
шаблон не подходит.
Сверяется текст между маркерами; ограда блока кода и пустые строки по краям в сверку не входят. Маркер, уехавший в проект вместе со скелетом, там полезен: он говорит, что у текста есть дом и правится он там.
Скрипт ловит четыре вещи: копия разошлась с домом (с диффом), копия указывает не на тот файл, дом остался без копий, разметка сломана. Чего он не ловит — копию, которую забыли пометить: помечать — по-прежнему решение человека.
Проверка диаграмм
Диаграммы mermaid живут исходником в markdown — картинок в репозитории нет.
Синтаксическая ошибка в блоке не видна при чтении: текст выглядит
правдоподобно, диff показывает разумную строку, а рендер падает.
uv run python scripts/diagrams.py # весь репозиторий
uv run python scripts/diagrams.py A.md B.md # только названные файлы
# 0 рендерятся, 1 нет, 3 нет mermaid-cli
Рендерит mmdc с PATH или npx --yes @mermaid-js/mermaid-cli; ни того ни
другого нет — код 3, а не молчаливый успех. Это самая дорогая проверка
репозитория: каждый блок — отдельный запуск mermaid-cli со своим chromium,
секунда с лишним. Поэтому у неё два рычага, и оба нужны гейту коммита: блоки
собираются все сразу, а рендерятся параллельно (пул потоков, порядок вывода
берётся из порядка сбора), и проверять можно названные файлы, а не весь
репозиторий. Весь репозиторий — три секунды вместо пятнадцати, один
файл — одна.
Чего проверка не ловит — расхождение диаграммы с прозой вокруг неё. Дословного соответствия между текстом и графом нет, сличать нечего, и держится это правилом старшинства, записанным рядом с каждой диаграммой: в конвейере ревью старший граф (он и есть алгоритм планировщика, проза его объясняет), в остальных местах старшая проза (диаграмма там сводка).
Гейт коммита
Все пять проверок стоят в pre-commit через lefthook —
конфиг в lefthook.yml, ставится один раз на клон:
lefthook install # пишет .git/hooks/pre-commit
lefthook run pre-commit # прогнать руками, не коммитя
| Проверка | Когда идёт | Что смотрит | Сколько |
|---|---|---|---|
| фронтматтеры | правка *.md |
весь репозиторий | миллисекунды |
| копии правил | правка *.md |
весь репозиторий | миллисекунды |
| диаграммы | правка *.md |
staged-файлы | ~1 с на файл |
ruff check --fix |
правка *.py |
staged-файлы | доли секунды |
pyrefly check |
правка *.py |
staged-файлы | доли секунды |
Glob разводит две половины: коммит, трогающий одни скрипты, не платит за рендер диаграмм, а коммит в документы не гоняет линтеры.
Судятся staged-файлы, а не рабочее дерево — гейт обязан проверять то, что
уедет в историю, а не то, что случайно лежит рядом на диске. Исключений два, и
оба про существо, а не про удобство: copies.py сверяет копию с домом, а дом
лежит в другом файле, которого в индексе может не быть (список staged дал бы
«копии дословны» ровно там, где правка дома их и разошлась), а frontmatter.py
обходит весь репозиторий за сотые доли секунды — экономить тут нечего.
ruff чинит безопасное сам, и починка доносится до этого же коммита
(stage_fixed: true). Иначе исправленный файл остался бы в рабочем дереве, а в
историю уехал бы невычищенный — худший из исходов: гейт зелёный, коммит грязный.
Обход разовый — LEFTHOOK=0 git commit …. Он законен ровно для случая,
когда найденное нечем чинить прямо сейчас; молча пропущенная проверка — нет.