diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index c166b58..08075e6 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -6,24 +6,24 @@ }, "plugins": [ { - "name": "av-dev-backlog", - "source": "./av-dev-backlog", - "description": "Ведение беклога задач как каталога markdown-файлов: заведение из диалога, разбор находок ревью, груминг, приоритизация, декомпозиция, штурм идей." - }, - { - "name": "av-dev-tasks", - "source": "./av-dev-tasks", - "description": "Управление задачами: цели вместо приоритетов, спринт под одну цель с заморозкой набора, вопросы и блокеры, каденция «разбор — переоценка — набор». Преемник av-dev-backlog." + "name": "av-dev-pm", + "source": "./av-dev-pm", + "description": "Управление продуктом: канон документов проекта, задачи и цели вместо приоритетов, спринт под одну цель с заморозкой набора, старт проекта интервью по брифу и приведение существующего к канону." }, { "name": "av-dev-pipeline", "source": "./av-dev-pipeline", - "description": "Проведение задачи через цикл SDD и конвейер ревью с обязательным триажем, плюс прогон нескольких задач разом. Проектная специфика — из файла-брифа." + "description": "Проведение задачи через цикл SDD и конвейер ревью с обязательным триажем, плюс прогон нескольких задач разом. Требует OpenSpec; проектная специфика — из документов канона." }, { "name": "av-dev-git", "source": "./av-dev-git", "description": "Git-обвязка для личных проектов. Скилл commit — сообщения коммитов в личном стиле (русский, scope-префикс, средняя детализация, без co-authored)." + }, + { + "name": "av-dev-backlog", + "source": "./av-dev-backlog", + "description": "УСТАРЕЛ, заменён плагином av-dev-pm. Старый формат беклога: один каталог задач с индексом README и приоритетами секциями. Оставлен до перевода последнего проекта; новые проекты не подключают." } ] } diff --git a/.gitignore b/.gitignore index 7a60b85..d646835 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,2 @@ -__pycache__/ *.pyc +__pycache__/ diff --git a/av-dev-backlog/.claude-plugin/plugin.json b/av-dev-backlog/.claude-plugin/plugin.json index 2201af9..36081b5 100644 --- a/av-dev-backlog/.claude-plugin/plugin.json +++ b/av-dev-backlog/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "av-dev-backlog", - "description": "Ведение беклога задач как каталога markdown-файлов (одна задача = один файл + строка в индексе README). Заведение, груминг, приоритизация, декомпозиция, штурм идей, разбор находок ревью.", + "description": "УСТАРЕЛ, заменён плагином av-dev-pm. Старый формат беклога: один каталог задач с индексом README и приоритетами секциями, без целей и спринтов. Оставлен до перевода последнего проекта, который на нём ещё живёт; новые проекты не подключают.", "author": { "name": "Anton Vakhrushev", "email": "anwinged@gmail.com" diff --git a/av-dev-backlog/skills/backlog/SKILL.md b/av-dev-backlog/skills/backlog/SKILL.md index 4826ac4..289921f 100644 --- a/av-dev-backlog/skills/backlog/SKILL.md +++ b/av-dev-backlog/skills/backlog/SKILL.md @@ -1,8 +1,14 @@ --- name: backlog -description: Работа с беклогом задач как с каталогом markdown-файлов (одна задача = один файл + строка в индексе README). Заведение задачи из диалога, разбор находок аудита/ревью в задачи, груминг (интерактивная чистка неактуального), приоритизация, декомпозиция на независимо полезные части, мозговой штурм идеи. Использовать, когда просят добавить задачу/идею в беклог, превратить находки ревью в задачи, разобрать беклог, расставить приоритеты, разбить задачу или проработать идею. Не реализует задачи — этим занимается пайплайн задачи проекта. +description: УСТАРЕЛ — используй скилл av-dev-pm:tasks. Старый формат беклога (один каталог задач, индекс README, приоритеты секциями, без целей и спринтов). Вызывать ТОЛЬКО в проекте, который на этот формат ещё не переведён, и только если прямо названо имя backlog. Во всех остальных случаях, включая любую просьбу завести задачу, идею или разобрать находки ревью, работает av-dev-pm:tasks. --- +> **Этот скилл устарел.** Формат заменён каноном `docs/tasks/` из плагина +> `av-dev-pm` (цели вместо приоритетов, спринт с заморозкой набора, `REJECTED.md` +> с причинами). Перевод проекта делает скилл `av-dev-pm:canon`. Скилл оставлен до +> перевода последнего проекта, который на нём ещё живёт, и будет удалён. + + # Беклог Беклог — каталог markdown-файлов: одна задача = один файл `.md`, плюс diff --git a/av-dev-pm/.claude-plugin/plugin.json b/av-dev-pm/.claude-plugin/plugin.json new file mode 100644 index 0000000..e60b434 --- /dev/null +++ b/av-dev-pm/.claude-plugin/plugin.json @@ -0,0 +1,8 @@ +{ + "name": "av-dev-pm", + "description": "Управление продуктом: канон документов проекта (паспорт, архитектура, схема БД, безопасность, конвенции, разведка, ADR, журнал ревью), задачи и цели вместо приоритетов, спринт под одну цель с заморозкой набора, старт нового проекта интервью по брифу и приведение существующего к канону. Не выполняет задачи — этим занимается пайплайн проекта.", + "author": { + "name": "Anton Vakhrushev", + "email": "anwinged@gmail.com" + } +} diff --git a/av-dev-pm/skills/canon/SKILL.md b/av-dev-pm/skills/canon/SKILL.md new file mode 100644 index 0000000..27adb74 --- /dev/null +++ b/av-dev-pm/skills/canon/SKILL.md @@ -0,0 +1,171 @@ +--- +name: canon +description: Привести проект к канону документов av-dev и держать его в соответствии — три операции одной машиной сравнения. check — что разошлось с текущей версией канона; adopt — перевод проекта из любой прежней раскладки (docs/specs, drafts, backlog, BRIEF.md, review-brief) в канон с переносом файлов; upgrade — повышение проекта с версии канона N до текущей по журналу версий. Использовать, когда просят проверить документацию проекта, перевести проект на канон, обновить его под новую версию канона или когда пришли в старый проект и надо понять, что в нём не так. Заведение нового проекта с нуля — скилл init. +--- + +# Приведение проекта к канону + +Три операции, одна машина сравнения с разными исходами: + +| Операция | Когда | Исход | +| --- | --- | --- | +| `check` | начало сессии, шаг синка, гейт | что разошлось | +| `adopt` | проект в чужой раскладке | перенос в канон | +| `upgrade` | канон вырос, проект отстал | по журналу версий | + +**Определение канона — [references/canon.md](references/canon.md).** Здесь оно не +пересказывается: два описания одной раскладки разъедутся, и работать будет то, +которое прочитали последним. Прочитай его **до** первой правки. + +Журнал версий — [references/changelog.md](references/changelog.md). + +## Три правила, из которых всё следует + +1. **Сперва карта, потом файлы.** Человеку показывается, что найдено, как + разложилось и **что не разложилось**, — и только после подтверждения + переносится хоть один файл. Массовый перенос без подтверждения разгребать + дороже, чем согласовать. +2. **Ничего не терять.** Содержимое переезжает целиком; ссылки чинятся тем же + проходом, что и перенос. Старый файл удаляется **только** после того, как + всё его содержимое нашло дом, и это названо поимённо. +3. **Что не классифицировалось — назвать.** Проглоченный абзац выглядит как + «всё перенеслось». Список «не разложилось» идёт в доклад целиком, с причиной + по каждому пункту. + +## Инструмент + +``` +ds="$CLAUDE_PLUGIN_ROOT/skills/canon/scripts/docs.py" + +python3 $ds check --dir <корень> [--base ] # раскладка, ссылки, версия, сверки +python3 $ds version --dir <корень> # версия канона скрипта и проекта +``` + +**Коды выхода — тот же словарь, что у `tasks.py`:** 0 сошлось, 1 дрейф, 2 ошибка +употребления, 3 окружение, 4 внутренний сбой. Ветвись на коде, а не на тексте. + +Различать 1 и 3 обязательно: «дрейф раскладки» — рабочая ситуация, «это не +корень проекта» — нерабочая. + +### Граница механизируемого — объявляется вслух + +Скрипт печатает её сам последним абзацем, и **эту строку из доклада выбрасывать +нельзя**. `check`, отчитавшийся «канон соблюдён» на проекте, где из шести файлов +три лишние, хуже отсутствующего. + +Машина проверяет пути, лишние файлы, битые ссылки, версию, нетронутые +плейсхолдеры, маркеры долга и две сверки с кодом. **Ты** судишь о том, чего она +не умеет: + +- **смысловой дубль** — `docs/specs/recognition.md` описывает то же, что + capability `recognition`. Файлы разные, содержание одно; +- **поведение, оставшееся в `architecture.md`** — раздел на 900 строк с + требованиями вместо обзора; +- **достаточность честной строки** — «внешних зависимостей нет» это факт, + «TBD» — пробел; +- **протухший факт** — документ ссылается на то, чего в коде уже нет. + +## `check` + +1. `docs.py check`, при наличии базы диффа — с `--base`. +2. Прочитай то, что скрипт проверить не может (список выше), по документам, + которых касалась работа. Не «заодно по всему `docs/`». +3. Доклад: вывод скрипта строкой исхода, твои находки поимённо, **граница + покрытия** — что смотрел и чего не смотрел. + +Дрейф раскладки чинится переносом; смысловые находки — это либо правка +документа, либо задача, если работы больше чем на абзац. + +## `adopt` — проект в чужой раскладке + +### 1. Осмотрись + +`docs.py check` — он уже назовёт упразднённые слоты с адресом, куда каждый +уезжает. Плюс прочитай: `CLAUDE.md`, корневые `*.md`, `openspec/specs/` (список +capability), `openspec/config.yaml`. + +### 2. Составь карту + +Каждый найденный файл получает строку: **куда едет, целиком или разбирается, что +делать с оригиналом**. Разбор `docs/specs/` — самое дорогое место, и он делается +поимённо по capability: + +| Что в файле | Куда | +| --- | --- | +| требования, сценарии, поведение | `openspec/specs//spec.md` — **или уже там**, тогда файл дубль | +| компоненты, транспорты, раскладка, деплой | `docs/architecture.md` | +| конвенции чужой системы, формат чужих данных | `docs/research/` | +| обоснование принятого решения | `docs/adr/` | + +**Дубль удаляется только после поимённой сверки**: открыть спеку capability, +открыть файл, убедиться, что в файле нет ничего сверх спеки. Нашлось сверх — +сперва переезжает в спеку дельтой, потом файл удаляется. + +### 3. Покажи карту человеку + +`AskUserQuestion`, **не больше трёх вопросов за итерацию**, рекомендация первым +вариантом. Показывается: сколько файлов, куда каждый, спорные отнесения, список +«не разложилось». Механику (порядок строк, имена файлов внутри `research/`) не +выноси — это не развилка. + +### 4. Перенеси + +Порядок важен — он минимизирует окно, в котором ссылки битые: + +1. `docs/.pm.json` с `{"canon": <текущая версия>}` и путём миграций, если БД есть; +2. каталоги канона и скелет: незаполненное — **одной честной информативной + строкой**, а не «TBD» (см. canon.md, «Пустое называется пустым»); +3. переносы содержимого; +4. каталог задач — **вызови скилл `av-dev-pm:tasks`**, сценарий адаптации: он + владеет форматом задач, включая переименование транслитных слагов в + английские вместе с починкой перекрёстных ссылок; +5. починка ссылок на перенесённое во всём репозитории — `docs/`, `openspec/`, + `CLAUDE.md`, `README.md`; +6. удаление оригиналов — **только тех, чьё содержимое найдено в новом доме**; +7. шаг `docs.py check` в гейт проекта; +8. `docs.py check` — до зелёного в механизируемой части. + +### 5. Объяви переходное состояние + +Сразу после переноса канон **заполнен не весь**, и это нормально, но обязано +быть названо, иначе следующий агент примет скелет за поломку. + +Печатается по факту: сколько документов стоят честной строкой вместо +содержания, сколько маркеров долга в `architecture.md`, сколько задач без +критериев приёмки. Закрывается порциями по ходу работы, а не одним заходом. + +## `upgrade` — канон вырос + +1. `docs.py version` — версия проекта и версия скрипта. +2. Проект новее скрипта — **обнови маркетплейс**, а не проект: это отстал + плагин. +3. Иначе иди по [changelog.md](references/changelog.md) снизу вверх от версии + проекта до текущей и делай названное в каждой записи. Записи независимы и + применяются по порядку. +4. Подними `canon` в `docs/.pm.json` до текущей. +5. `docs.py check`. + +Записи журнала описывают **что сделать проекту**. Если запись этого не говорит — +это дефект журнала, и о нём надо сказать, а не догадываться. + +## Чего этот скилл не делает + +- **Не сочиняет содержание.** Пустой слот получает честную строку о том, что его + наполнить пока нечем, а не выдуманный абзац. Придуманный периметр модели угроз + хуже отсутствующего: по нему будут строиться находки. +- **Не удаляет то, чьё содержимое не нашло дом.** Оригинал живёт, пока не + названо поимённо, куда переехал каждый его кусок. +- **Не ведёт содержимое канона** — это скилл `docs`. Здесь только раскладка. +- **Не заводит проект с нуля** — это скилл `init`. +- **Не правит историю.** В старых коммитах старые пути остаются, и это нормально. + +## Доклад + +- Что нашёл `docs.py`: код выхода и число пунктов дрейфа. +- Что перенесено: файл → дом, числом и поимённо для спорного. +- **Удалённые дубли** — с указанием, против какой спеки сверялся каждый. +- **Не разложилось** — поимённо, с причиной. +- Переходное состояние числами: честных строк, маркеров долга, задач без + критериев. +- **Граница покрытия**: что проверила машина, что судил ты, чего не смотрел + никто. diff --git a/av-dev-pm/skills/canon/references/canon.md b/av-dev-pm/skills/canon/references/canon.md new file mode 100644 index 0000000..d8a2819 --- /dev/null +++ b/av-dev-pm/skills/canon/references/canon.md @@ -0,0 +1,273 @@ +# Канон документов проекта + +**Версия 1.** + +Это **единственный дом определения канона**. Скиллы `init`, `canon` и `docs` +читают его, а не пересказывают: три описания одной раскладки разъедутся, и +работать будет то, которое прочитали последним. Меняется канон — меняется этот +файл и появляется запись в [changelog.md](changelog.md). + +## Зачем канон жёсткий + +Пути фиксированы, и проект под них подгоняется, а не наоборот. Причина не +техническая: проектов много, все малого и среднего размера, и ориентироваться в +слегка похожих, но разных раскладках дороже, чем один раз привести их к общей. +Рядом лежит OpenSpec, у которого структура тоже строгая. + +Цена принята сознательно: плагин не переносится на чужой репозиторий как есть — +чужой репозиторий **приводится** к канону скиллом `canon`. + +## Раскладка + +``` +CLAUDE.md памятка агенту: что это, стек, инварианты с + severity, команды, семантика гейта, запреты +docs/ + .pm.json версия канона и пути, нужные проверкам + passport.md зачем и для кого; чем НЕ является; сценарии + architecture.md как сложено — обзор; окружение и эксплуатация + database.md схема хранилища; представление данных и настройки + security.md периметр; недоверенный вход; что вне модели + conventions/ + README.md индекс, правило промоута, что механизировано + <тема>.md + research/ + README.md как снималось, индекс + <тема>.md наблюдения и числа с провенансом + adr/ + README.md индекс записей, статусы, правило замены + template.md + ADR-ГГГГ-ММ-ДД-slug.md + review.md настройка конвейера под проект + журнал дефектов + tasks/ скилл tasks: items/, PLAN.md, BACKLOG.md, + SPRINT.md, REJECTED.md +openspec/ + config.yaml только нужды генерации артефактов + ссылки + specs//spec.md что система делает — нормативно + changes/archive/ архив изменений с design.md — сырьё для ADR +``` + +Текст документов — русский; слаги файлов, capability и задач — английские, +kebab-case. + +## Роли документов + +Одна строка на каждый — на какой вопрос он отвечает и кто его читает. + +| Документ | Вопрос | Кто читает, кроме человека | +| --- | --- | --- | +| `CLAUDE.md` | что нельзя нарушать, чем краснеет гейт | все агенты, всегда | +| `passport.md` | зачем и для кого, чем это **не** является | `architecture`, `rubric`, `reimpl`, `specs` | +| `architecture.md` | как сложено и где что работает | все проходы ревью | +| `database.md` | что лежит в хранилище и какими настройками | `ops`, `adversary`, `reimpl` | +| `security.md` | против кого защищаемся и что вне модели | `adversary` | +| `conventions/` | как мы пишем код | `code` | +| `research/` | что показала реальность, а не документация | `specs`, `reimpl`, `ops` | +| `adr/` | почему решено именно так | `architecture` | +| `review.md` | как настроен конвейер и что уже проскакивало | `triage`, каждый проход — свою часть | +| `openspec/specs/` | что система делает — нормативно | `specs` | + +### `passport.md` + +Цель; закрытый список потребителей и что каждому нужно; **чем целью не +является** — это граница домена, по которой архитектурный проход судит о +переносе понятия; типовые сценарии; мера, по которой проект считается удавшимся; +референсы, у кого подсматривать. + +### `architecture.md` — **обзор, не поведение** + +Принципы; компоненты **со ссылками на capability**, а не с пересказом их +требований; внешние границы и форматы чужих систем; окружение — где работает, +что рядом, кто перезапускает; **внешние зависимости поимённо** и чем каждая +отказывает (не только «падает», но и «отвечает медленно», «молчит», «отдаёт +мусор»); кто заметит отказ и когда; характер потока — непрерывный, по запросу, +по расписанию; что обратимо, а что нет; деплой; открытые вопросы. + +**Поведение системы сюда не пишется.** Его нормативный дом — `openspec/specs/`, +куда `opsx:archive` вливает дельты; второй дом синхронизировать руками +невозможно, и он разойдётся. + +Раздел, ещё не разнесённый при переезде, помечается маркером долга: + +``` + +``` + +`docs.py` считает маркеры и печатает остаток числом. Гейт от них **не краснеет**: +это долг, а не отказ, иначе постепенный переезд стал бы невозможен. + +### `database.md` + +Схема: таблицы, ключи, связи, правило времени и идентификаторов. Плюс то, чего +нет в схеме, но без чего замер не превращается в находку: **чем физически лежит +запись** (сжатый BLOB, JSON-строка, колонки), что происходит при чтении и записи +(распаковка целиком, read-modify-write), и **настройки с числовым значением** — +таймаут занятости, режим журналирования, лимит тела, размер пула, ретеншен. + +Конвенции идентификаторов и именования — не схема, они в `conventions/`. + +### `security.md` + +**Периметр первой строкой.** «Сервис открыт наружу» и «контур доверенный, +публичного интернета здесь нет, не выдумывай его» — противоположные постановки +под одним заголовком, и враждебный проход между ними сам не выберет. Контур ещё +не развёрнут — назови **оба** периметра, целевой и сегодняшний, и скажи прямо, +против какого строятся находки. + +Дальше: что недоверенное и каким каналом приходит; **из чего строятся пути и +ключи** (раскладка файлов, состав координатного ключа, имя каталога) — отсюда +строится выход за пределы песочницы; что разграничивает доступ; что +чувствительнее чего; **что вне модели** — перечислить явно. + +### `conventions/` + +Прозой остаётся **только то, что не выражается правилом**. `README.md` держит +индекс, правило промоута и **перечень уже механизированного** со ссылкой на +место механизации — конфиг линтера, собственный анализатор, тест-сканер +исходников. Непойманное место механизации означает, что проход добросовестно +проверит уже проверенное. + +### `research/` + +Наблюдения за внешним миром: что реально шлёт источник, чем документация формата +расходится с практикой, какие числа сняты с живого потока. **Числа — с +провенансом**, то есть с командой или условиями, которыми получены. +`README.md` — как снималось и индекс тем. + +Число без источника проход обязан читать как условие, а не как замер. Число, чей +источник по ссылке не подтвердился, не выбрасывается и не переписывается по +догадке — остаётся с пометкой «расходится с источником: там <что нашли>». + +### `adr/` + +**ADR — промоут поверх архивных `design.md`, а не второе сочинение.** Запись +цитирует решение и ссылается на `openspec/changes/archive//design.md`. + +Заводится, когда верно одно из трёх: + +- **дорогой откат** — переделка стоит дороже переписывания одного файла; +- **намеренный отказ** от очевидного подхода; +- **пересмотр прежнего решения** — тогда у старой записи обязателен статус + «заменено на». + +Не заводится для рутины и для того, что видно из кода и `git log`. + +Записи неизменяемы: передумали — заводится новая, старая получает статус. +Активная запись статуса не имеет. + +### `review.md` + +Два раздела с разными сроками жизни. + +**Настройка конвейера под проект:** типовые узлы (рода узлов и 3–5 проверяемых +свойств к каждому); типовые ложноположительные — находки, которые здесь выглядят +убедительно и всегда неверны; вопросы к проходам поимённо с провенансом; +недоступно проверке — два подраздела, «не проверит ни один проход» +(принципиальная граница, по факту промаха не пересматривается) и «перестали +проверять сознательно» (пересматривается первым). + +**Журнал дефектов:** запись на каждый воспроизведённый дефект с пометкой +**проскочил / пойман ревью**. Проскочившие — эвал-сет для калибровки конвейера, +выборка по пометке. Пойманные с оракулом — лучшая опора для прохода: проектные, +воспроизводимые, однажды оказавшиеся правдой. + +### `CLAUDE.md` + +Что это и стек; **инварианты с severity рядом с формулировкой** — по ним +проходы присваивают `critical`, поэтому severity стоит здесь, а не выводится +каждым проходом заново; команды; **семантика гейта** — чем краснеет безусловно и +почему, где логи, что означает исход, чего в гейте намеренно нет, **кто и когда +обязан гонять дорогое вне гейта**; что запускать запрещено, с путями; что +считается необратимым; общий станок, врывающийся в замороженный спринт; ориентир +по размеру спринта. + +### `openspec/config.yaml` + +**Только нужды генерации артефактов** — язык, правила именования capability, +придирки валидатора RFC 2119 — плюс ссылки на документы канона. Правило ревью, +пересказ конвенций и инварианты сюда не пишутся: у них есть свои дома, и второй +дом разойдётся на первой же правке. + +## Правило единственного дома + +Факт живёт ровно в одном файле; остальные ссылаются. Карта на случай спора: + +| Факт | Дом | +| --- | --- | +| поведение системы | `openspec/specs//spec.md` | +| почему решено так | `adr/`, источник — архивный `design.md` | +| граница домена, «чем не является» | `passport.md` | +| инвариант и его severity | `CLAUDE.md` | +| порядок работ и его обоснование | `docs/tasks/PLAN.md` | +| измеренное число | `research/` | +| настройка с числовым значением | `database.md` | +| периметр и модель угроз | `security.md` | +| что уже механизировано правилом | `conventions/README.md` | + +## Пустое называется пустым + +Скелет канона заводится **целиком** с первого дня. Незаполненный документ держит +**одну честную информативную строку**, а не заглушку: + +- «внешних зависимостей нет — смотри на диск и на СУБД»; +- «наблюдений на живых данных нет: внешний источник один, формат документирован»; +- «прецедентов не накоплено»; +- «сознательно ничего не отключали»; +- «архитектуры пока нет: кода нет, заводится первой задачей». + +Проход читает такую строку **как факт** и не тратит на неё обязательный вопрос. +Отсутствие файла он не может прочитать никак, а «TBD» читает как пробел — +поэтому `docs.py check` отличает честную строку от нетронутого плейсхолдера +шаблона и напоминает о втором. + +## Слотов нет + +Файлы и каталоги, которых в каноне **нет**, и куда уезжает их содержимое: + +| Было | Куда | +| --- | --- | +| `docs/review-brief.md` | документы канона и есть бриф; остаток — в `review.md` | +| `docs/specs/` | `openspec/specs/` (поведение) и `architecture.md` (обзор) | +| `docs/drafts/` | идея → задача `[idea]`; отказ → ADR; порядок → `PLAN.md`; размышление → `opsx:explore` | +| `docs/plan.md` | `docs/tasks/PLAN.md` | +| `BRIEF.md` | `passport.md` | +| `docs/backlog/` | `docs/tasks/` | +| `docs/review-journal.md`, `docs/review/journal.md` | `docs/review.md` | + +## Что проверяет машина, а что человек + +Граница объявляется вслух в каждом отчёте: `check`, отчитавшийся «канон +соблюдён» на проекте, где из шести файлов три лишние, хуже отсутствующего. + +| Проверяет `docs.py` | Судит агент | +| --- | --- | +| отсутствующие пути канона | смысловой дубль документа и capability | +| файлы в `docs/` вне канона | поведение, оставшееся в `architecture.md` | +| битые относительные ссылки | протухший факт, разошедшийся с кодом | +| версия канона и её отставание | достаточность честной строки в пустом слоте | +| нетронутый плейсхолдер шаблона | связность и читаемость | +| маркеры долга — числом | | +| миграция изменена, а `database.md` нет | | +| capability без упоминания в `architecture.md` | | + +## `docs/.pm.json` + +```json +{ + "canon": 1, + "migrations": "internal/store/migrations", + "tasks": { + "sections": ["ядро", "инфра"] + } +} +``` + +`canon` — версия канона, под которую проект приведён, целым числом: обратной +совместимости у канона нет, есть «приведён» и «не приведён». `migrations` — путь +каталога миграций, если БД есть; по нему `docs.py` делает сверку с +`database.md`. `tasks` — настройки каталога задач, переехавшие сюда из прежнего +`/.tasks.json`: **один конфиг на весь канон, а не по одному на каталог**. + +Ключей будет больше по мере роста проверок; неизвестный ключ `docs.py` +игнорирует, отсутствующий — считает «проверка неприменима» и говорит об этом +строкой, а не молчит. diff --git a/av-dev-pm/skills/canon/references/changelog.md b/av-dev-pm/skills/canon/references/changelog.md new file mode 100644 index 0000000..e7c4795 --- /dev/null +++ b/av-dev-pm/skills/canon/references/changelog.md @@ -0,0 +1,44 @@ +# Журнал версий канона + +Одна запись на версию. Проект знает свою версию из `docs/.pm.json`; `canon +upgrade` идёт по записям снизу вверх от версии проекта до текущей и делает то, +что в них названо. + +Правило записи: **что добавилось, что переехало, что удалено, что сделать +проекту**. Без последнего пункта запись бесполезна — по ней и работает +`upgrade`. + +Версия — целое число. Обратной совместимости у канона нет: есть «приведён» и «не +приведён». + +--- + +## Версия 1 — 2026-08-03 + +Первая версия. Проект любой прежней раскладки приводится к ней скиллом `canon` +в режиме `adopt`, а не `upgrade`. + +**Что вводится:** раскладка целиком — см. [canon.md](canon.md). + +**Что сделать проекту, который приходит из свободной раскладки:** + +1. `docs/.pm.json` с `{"canon": 1}` и путём миграций, если БД есть. +2. Скелет канона целиком; незаполненное — одной честной строкой. +3. `docs/specs/` разобрать: поведение — в `openspec/specs/`, обзор — в + `docs/architecture.md`, знание о чужих системах — в `docs/research/`. + Дубли capability удалить, сверив поимённо. +4. `docs/plan.md` → `docs/tasks/PLAN.md` линией целей. +5. `BRIEF.md` → `docs/passport.md`. +6. `docs/backlog/` → `docs/tasks/`. +7. `docs/review-journal.md` или `docs/review/journal.md` → `docs/review.md`, + плюс раздел настройки конвейера. +8. `docs/drafts/` растворить: идея → задача `[idea]`, намеренный отказ → ADR, + порядок работ → `PLAN.md`. +9. `docs/review-brief.md`, если заводился, удалить: его разделы разошлись по + документам канона. +10. `conventions.md` → `conventions/`, `local-research.md` → `research/`. +11. Завести `docs/security.md` с периметром первой строкой и `docs/adr/`. +12. В `CLAUDE.md`: severity рядом с каждым инвариантом, семантика гейта, + запреты с путями; убрать раздел «Процесс», если он пересказывает пайплайн. +13. В `openspec/config.yaml` оставить только нужды генерации и ссылки. +14. Добавить шаг `docs.py check` в гейт проекта. diff --git a/av-dev-pm/skills/canon/references/skeletons.md b/av-dev-pm/skills/canon/references/skeletons.md new file mode 100644 index 0000000..b25b0bb --- /dev/null +++ b/av-dev-pm/skills/canon/references/skeletons.md @@ -0,0 +1,291 @@ +# Скелеты документов канона + +Что кладут `init` и `canon adopt` в незаполненный слот. Правило одно: +**честная информативная строка вместо заглушки**. Проход читает строку как факт; +`` он читает как пробел, и `docs.py check` о таком +плейсхолдере напоминает. + +Плейсхолдер ставится только там, где ответ **обязан** быть и его не спросили. +Всё, чего в проекте пока просто нет, описывается словами, а не плейсхолдером. + +## `docs/passport.md` + +```markdown +# Паспорт проекта + +Зачем это и для кого. [architecture.md](architecture.md) отвечает «как +устроено», [tasks/PLAN.md](tasks/PLAN.md) — «в каком порядке», паспорт — +«зачем и для кого». + +## Цель + + + +**Потребители** — список закрытый: он определяет, что считать нужным, а что +интересным. + +| Кто | Что ему нужно от нас | +| --- | --- | + +Цель достигнута, когда: + +## Что целью не является + +Граница домена. По ней архитектурный проход судит, не перенесено ли понятие +через границу. + +## Типовые сценарии + +## Референсы + +Где смотреть prior art, когда упёрлись. +``` + +## `docs/architecture.md` + +```markdown +# Архитектура + +Обзор: как сложено и где что работает. **Поведение системы здесь не +описывается** — его нормативный дом `openspec/specs/`. + +## Принципы + +## Компоненты + +Каждый — строкой со ссылкой на capability, а не пересказом её требований. + +## Внешние границы и форматы + +## Эксплуатация + +- Где работает, что рядом, кто перезапускает: +- Внешние зависимости поимённо и чем каждая отказывает (падает, отвечает + медленно, молчит, отдаёт мусор): +- Кто заметит отказ и когда: +- Характер потока (непрерывный, по запросу, по расписанию): +- Что обратимо, а что нет: + +## Деплой + +## Открытые вопросы +``` + +Пустой проект: «Архитектуры пока нет: кода нет. Наполняется первой задачей.» +Нет внешних зависимостей: «Внешних зависимостей нет — смотри на диск и на СУБД.» + +## `docs/database.md` + +```markdown +# Схема хранилища + +СУБД, миграции, правило времени и идентификаторов. + +## Таблицы + +## Представление данных + +Чем физически лежит запись и что происходит при чтении и записи. + +## Настройки с числовым значением + +Таймаут занятости, режим журналирования, лимит тела, размер пула, ретеншен. +Без них замер не превращается в находку: пик памяти — аномалия только рядом +со строкой «запись лежит сжатой и распаковывается целиком». +``` + +Нет БД — файла нет, и в `docs/.pm.json` нет ключа `migrations`. + +## `docs/security.md` + +```markdown +# Модель угроз + +## Периметр + + + +Контур не развёрнут — назови оба периметра, целевой и сегодняшний, и скажи +прямо, против какого строятся находки. + +## Недоверенный вход + +Что приходит извне и каким каналом: тело запроса, файл, аргумент команды, +ответ внешней системы, содержимое архива. + +## Из чего строятся пути и ключи + +Раскладка файлов на диске, состав координатного ключа записи, имя каталога. +Отсюда строится выход за пределы песочницы. + +## Что разграничивает доступ + +## Что чувствительнее чего + +## Что вне модели + +Перечислить явно. Пустой пункт означает, что враждебный проход выдумает угрозу +сам, и находка никогда не будет исправлена. +``` + +## `docs/conventions/README.md` + +```markdown +# Конвенции кода + +Как мы пишем код — в отличие от `openspec/specs/`, который описывает, что +система делает. + +**Прозой остаётся только то, что не выражается правилом.** Свойство, ставшее +правилом линтера, отсюда удаляется и переезжает в перечень ниже. + +## Записи + +## Механизировано + +| Правило | Где механизировано | +| --- | --- | + +Непойманное место механизации означает, что проход по конвенциям будет +добросовестно проверять уже проверенное. +``` + +Пустой проект: «Конвенций пока нет: код не написан. Наполняется по мере +реального трения, а не вперёд.» + +## `docs/research/README.md` + +```markdown +# Разведка + +Наблюдения за внешним миром: что реально шлёт источник, чем документация +формата расходится с практикой. Источник истины — этот каталог, а не чужая +документация. + +**Каждый вывод — с числами и командой, которой получен**, чтобы его можно было +перепроверить. + +## Как снималось + +## Записи +``` + +Нет внешних источников: «Внешних источников данных нет — разведка неприменима.» + +## `docs/adr/README.md` + +```markdown +# Журнал решений + +Одна запись — одно решение. **ADR это промоут поверх архивного `design.md`**, +а не второе сочинение: запись цитирует решение и ссылается на +`openspec/changes/archive//design.md`. + +## Когда заводить + +Верно одно из трёх: + +- **дорогой откат** — переделка стоит дороже переписывания одного файла; +- **намеренный отказ** от очевидного подхода; +- **пересмотр прежнего решения** — тогда у старой записи обязателен статус. + +Не заводить для рутины и того, что видно из кода и `git log`. + +## Соглашения + +- Имя файла — `ADR-ГГГГ-ММ-ДД-slug.md`, слаг английский, дата — когда решение + реально принято. +- Записи неизменяемы: передумали — новая запись, старой ставится статус. +- Активная запись статуса не имеет. Значений два: `заменено на ADR-…` и + `устарело`. + +## Записи + +Новые сверху. + +| Дата | Запись | Статус | +| --- | --- | --- | +``` + +## `docs/adr/template.md` + +```markdown +# Краткий заголовок решения + +- Дата: ГГГГ-ММ-ДД +- Источник: openspec/changes/archive//design.md + +## Решение + +Что именно решено — одной фразой. + +## Почему + +Намерение и причина. Цитата из источника, а не пересказ. Пиши так, чтобы через +год было понятно без чтения переписки. + +## Последствия + +- `+` что стало лучше. +- `−` чем платим: ограничения, риски, нагрузка на поддержку. +``` + +## `docs/review.md` + +```markdown +# Ревью: настройка и журнал + +## Как настроен конвейер + +### Типовые узлы + +Рода узлов проекта и 3–5 проверяемых свойств к каждому. Рода, а не инвентарь +пакетов: род, который проект задумал, но ещё не написал, включать полезно. + +### Типовые ложноположительные + +Находки, которые здесь выглядят убедительно и всегда неверны. Каждая — с одной +строкой «почему здесь это не дефект». + +### Вопросы к проходам + +Форма: `<имя прохода>: <вопрос> (<провенанс>)`. Главный источник — журнал ниже. + +### Недоступно проверке + +**Не проверит ни один проход** — принципиальная граница; по факту промаха не +пересматривается. + +**Перестали проверять сознательно** — что, когда и почему, со ссылкой на запись +журнала. Пересматривается **первым**, как только что-то проскочило. + +## Журнал дефектов + +Запись на каждый воспроизведённый дефект, сразу, а не ретроспективно: со +временем теряется не факт, а причина непоймания. + +Форма: + +## ГГГГ-ММ-ДД — краткое последствие [проскочил|пойман] + +- **Где:** файл:строка +- **Симптом:** как обнаружилось +- **Чем воспроизведён:** тест, команда, замер +- **Почему не поймали:** только для проскочивших +- **Что меняем:** правило прохода, шаг гейта, конвенция — либо «ничего, цена + поимки выше цены дефекта» +``` + +Новый проект: «Дефектов пока не было. Настройка конвейера появится с первым +ревью.» + +## `docs/.pm.json` + +```json +{ + "canon": 1 +} +``` + +Плюс `"migrations": "<путь>"`, если есть БД, и `"tasks": {"sections": [...]}`, +если секции беклога отличаются от умолчания. diff --git a/av-dev-pm/skills/canon/scripts/docs.py b/av-dev-pm/skills/canon/scripts/docs.py new file mode 100644 index 0000000..acababc --- /dev/null +++ b/av-dev-pm/skills/canon/scripts/docs.py @@ -0,0 +1,393 @@ +#!/usr/bin/env python3 +"""Проверка раскладки документов проекта против канона av-dev. + +Определение канона — references/canon.md рядом со скриптом. Здесь только +механизируемая часть: пути, лишние файлы, битые ссылки, версия, плейсхолдеры, +маркеры долга и две сверки с кодом. Смысловые дубли и оставшееся в архитектуре +поведение судит агент — скрипт об этом говорит вслух в конце отчёта. + +Коды выхода — тот же словарь, что у tasks.py: + 0 сошлось + 1 дрейф раскладки (рабочая ситуация, чинится) + 2 ошибка употребления + 3 окружение: не тот каталог, битый конфиг + 4 внутренний сбой +""" + +from __future__ import annotations + +import argparse +import json +import re +import subprocess +import sys +from dataclasses import dataclass, field +from pathlib import Path + +CANON_VERSION = 1 + +OK, DRIFT, USAGE, ENV, INTERNAL = 0, 1, 2, 3, 4 + +# --- Раскладка канона ------------------------------------------------------- + +# Обязательные файлы: путь → на какой вопрос отвечает (для внятного отказа). +REQUIRED = { + "CLAUDE.md": "памятка агенту: инварианты с severity, команды, семантика гейта", + "docs/.pm.json": "версия канона и пути, нужные проверкам", + "docs/passport.md": "зачем и для кого, чем НЕ является", + "docs/architecture.md": "как сложено — обзор, окружение, эксплуатация", + "docs/security.md": "периметр, недоверенный вход, что вне модели", + "docs/review.md": "настройка конвейера + журнал дефектов", + "docs/conventions/README.md": "индекс конвенций, правило промоута, что механизировано", + "docs/research/README.md": "как снималось, индекс наблюдений", + "docs/adr/README.md": "индекс записей, статусы, правило замены", + "docs/adr/template.md": "шаблон записи ADR", +} + +# Обязателен только при условии: путь → (ключ .pm.json, пояснение). +CONDITIONAL = { + "docs/database.md": ("migrations", "схема хранилища и настройки"), +} + +# Что вообще разрешено лежать в docs/ верхним уровнем. +ALLOWED_FILES = { + ".pm.json", + "passport.md", + "architecture.md", + "database.md", + "security.md", + "review.md", +} +ALLOWED_DIRS = {"conventions", "research", "adr", "tasks"} + +# Слоты, которых в каноне нет, — с адресом, куда уезжает содержимое. +RETIRED = { + "review-brief.md": "документы канона и есть бриф; остаток — в review.md", + "review-journal.md": "→ docs/review.md", + "plan.md": "→ docs/tasks/PLAN.md", + "conventions.md": "→ docs/conventions/", + "local-research.md": "→ docs/research/", + "research.md": "→ docs/research/", + "specs": "поведение → openspec/specs/, обзор → docs/architecture.md", + "drafts": "идея → задача [idea], отказ → ADR, порядок → PLAN.md", + "backlog": "→ docs/tasks/", + "review": "→ docs/review.md", +} + +DEBT_MARKER = re.compile(r"") +PLACEHOLDER = re.compile(r"") +MD_LINK = re.compile(r"\[[^\]]*\]\(([^)]+)\)") +FENCE = re.compile(r"^\s*(```|~~~)") + + +def strip_code(text: str) -> str: + """Выкинуть блоки кода: пути в примерах и шаблонах — не ссылки, и краснеть + на них значит краснеть на каждом образце документа.""" + out, inside = [], False + for line in text.splitlines(): + if FENCE.match(line): + inside = not inside + continue + out.append("" if inside else line) + return "\n".join(out) + + +@dataclass +class Report: + errors: list[str] = field(default_factory=list) + notes: list[str] = field(default_factory=list) + debts: list[str] = field(default_factory=list) + skipped: list[str] = field(default_factory=list) + + def error(self, msg: str) -> None: + self.errors.append(msg) + + def note(self, msg: str) -> None: + self.notes.append(msg) + + def debt(self, msg: str) -> None: + self.debts.append(msg) + + def skip(self, msg: str) -> None: + self.skipped.append(msg) + + +def fail(code: int, msg: str) -> None: + print(f"ОТКАЗ: {msg}", file=sys.stderr) + sys.exit(code) + + +def read_config(root: Path, rep: Report) -> dict: + path = root / "docs" / ".pm.json" + if not path.exists(): + return {} + try: + data = json.loads(path.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + fail(ENV, f"docs/.pm.json не разбирается: {exc}") + if not isinstance(data, dict): + fail(ENV, "docs/.pm.json должен быть объектом") + return data + + +# --- Проверки --------------------------------------------------------------- + + +def check_version(root: Path, cfg: dict, rep: Report) -> None: + if not (root / "docs" / ".pm.json").exists(): + return # об отсутствии файла скажет check_required, второй раз не нужно + if "canon" not in cfg: + rep.error("в docs/.pm.json нет ключа canon — версия канона не объявлена") + return + got = cfg["canon"] + if not isinstance(got, int): + rep.error(f"canon в docs/.pm.json должен быть целым числом, а не {got!r}") + return + if got < CANON_VERSION: + rep.error( + f"проект приведён к канону версии {got}, текущая — {CANON_VERSION}: " + f"нужен canon upgrade" + ) + elif got > CANON_VERSION: + rep.error( + f"проект приведён к канону версии {got}, а скрипт знает {CANON_VERSION}: " + f"устарел плагин, обнови маркетплейс" + ) + + +def check_required(root: Path, cfg: dict, rep: Report) -> None: + for rel, what in REQUIRED.items(): + if not (root / rel).exists(): + rep.error(f"нет {rel} — {what}") + for rel, (key, what) in CONDITIONAL.items(): + if key in cfg and not (root / rel).exists(): + rep.error(f"нет {rel} — {what} (обязателен: в .pm.json объявлен {key})") + elif key not in cfg and not (root / rel).exists(): + rep.skip(f"{rel} — в .pm.json нет ключа {key}, проверка неприменима") + + +def check_stray(root: Path, rep: Report) -> None: + docs = root / "docs" + if not docs.is_dir(): + rep.error("нет каталога docs/") + return + for entry in sorted(docs.iterdir()): + name = entry.name + if name in RETIRED: + rep.error(f"docs/{name} — слота нет в каноне: {RETIRED[name]}") + continue + if entry.is_dir(): + if name not in ALLOWED_DIRS: + rep.error(f"docs/{name}/ — каталог вне канона") + elif name not in ALLOWED_FILES: + rep.error(f"docs/{name} — файл вне канона") + + +def canon_docs(root: Path) -> list[Path]: + """Документы канона. Каталог задач ведёт tasks.py; упразднённые каталоги + уже названы отдельной строкой, и их внутренние ссылки не наша забота — + они переезжают целиком.""" + out = [] + docs = root / "docs" + skip = {"tasks"} | {name for name in RETIRED if not name.endswith(".md")} + if docs.is_dir(): + for path in sorted(docs.rglob("*.md")): + head = path.relative_to(docs).parts[0] + if head in skip or head in RETIRED: + continue + out.append(path) + claude = root / "CLAUDE.md" + if claude.exists(): + out.append(claude) + return out + + +def check_links(root: Path, rep: Report) -> None: + for path in canon_docs(root): + try: + text = path.read_text(encoding="utf-8") + except OSError as exc: + rep.error(f"{path.relative_to(root)} не читается: {exc}") + continue + for target in MD_LINK.findall(strip_code(text)): + target = target.strip() + if not target or target.startswith(("http://", "https://", "#", "mailto:")): + continue + clean = target.split("#", 1)[0] + if not clean: + continue + if (path.parent / clean).exists(): + continue + rep.error(f"{path.relative_to(root)}: битая ссылка на {target}") + + +def check_placeholders_and_debt(root: Path, rep: Report) -> None: + for path in canon_docs(root): + text = strip_code(path.read_text(encoding="utf-8", errors="replace")) + rel = path.relative_to(root) + for what in PLACEHOLDER.findall(text): + rep.error(f"{rel}: плейсхолдер шаблона не заполнен — {what}") + for what in DEBT_MARKER.findall(text): + rep.debt(f"{rel}: {what}") + + +def check_capabilities(root: Path, rep: Report) -> None: + specs = root / "openspec" / "specs" + arch = root / "docs" / "architecture.md" + if not specs.is_dir(): + rep.skip("openspec/specs/ нет — сверка capability с архитектурой неприменима") + return + if not arch.exists(): + return + text = arch.read_text(encoding="utf-8", errors="replace") + missing = [d.name for d in sorted(specs.iterdir()) if d.is_dir() and d.name not in text] + for name in missing: + rep.error( + f"capability {name} есть в openspec/specs/, но не упомянута в " + f"docs/architecture.md — обзор отстал от нормативных спек" + ) + + +def changed_files(root: Path, base: str, rep: Report) -> list[str] | None: + try: + out = subprocess.run( + ["git", "-C", str(root), "diff", "--name-only", f"{base}...HEAD"], + capture_output=True, + text=True, + check=True, + ) + except (subprocess.CalledProcessError, FileNotFoundError) as exc: + rep.skip(f"сверка миграций пропущена: git не отдал дифф ({exc})") + return None + return [line for line in out.stdout.splitlines() if line] + + +def check_migrations(root: Path, cfg: dict, base: str | None, rep: Report) -> None: + migrations = cfg.get("migrations") + if not migrations: + rep.skip("в .pm.json нет ключа migrations — сверка со схемой неприменима") + return + if not base: + rep.skip("база диффа не названа (--base) — сверка миграций со схемой не гонялась") + return + changed = changed_files(root, base, rep) + if changed is None: + return + touched = [f for f in changed if f.startswith(migrations.rstrip("/") + "/")] + if not touched: + return + if "docs/database.md" not in changed: + rep.error( + f"миграции изменены ({len(touched)} файлов), а docs/database.md — нет: " + f"схема в документации отстала" + ) + + +def check_tasks(root: Path, rep: Report) -> None: + tasks = root / "docs" / "tasks" + if not tasks.is_dir(): + rep.error("нет docs/tasks/ — каталог задач часть канона") + return + script = Path(__file__).resolve().parents[2] / "tasks" / "scripts" / "tasks.py" + if not script.exists(): + rep.skip(f"tasks.py не найден по пути {script} — согласованность задач не проверена") + return + proc = subprocess.run( + [sys.executable, str(script), "check", "--dir", str(tasks)], + capture_output=True, + text=True, + ) + if proc.returncode == 0: + return + if proc.returncode == 1: + rep.error("tasks.py check нашёл дрейф в docs/tasks/ — разбирать его командой tasks.py") + else: + rep.error(f"tasks.py check отказал с кодом {proc.returncode}: {proc.stderr.strip()}") + + +# --- Отчёт ------------------------------------------------------------------ + + +def report(rep: Report) -> int: + for msg in rep.errors: + print(f"ДРЕЙФ {msg}") + for msg in rep.notes: + print(f"ЗАМЕЧАНИЕ {msg}") + if rep.debts: + print(f"\nДОЛГ ({len(rep.debts)} маркеров, гейт от них не краснеет):") + for msg in rep.debts: + print(f" {msg}") + if rep.skipped: + print("\nНЕ ПРОВЕРЯЛОСЬ:") + for msg in rep.skipped: + print(f" {msg}") + + print( + "\nМашина проверила раскладку, ссылки, версию и две сверки с кодом.\n" + "Смысловые дубли, оставшееся в architecture.md поведение и достаточность\n" + "честной строки в пустом слоте она не проверяет — это суждение агента." + ) + if rep.errors: + print(f"\nИтог: дрейф, {len(rep.errors)} пунктов.") + return DRIFT + print("\nИтог: канон соблюдён в механизируемой части.") + return OK + + +def cmd_check(args: argparse.Namespace) -> int: + root = Path(args.dir).resolve() + if not root.is_dir(): + fail(ENV, f"каталог {root} не найден") + if not (root / "docs").exists() and not (root / "CLAUDE.md").exists(): + fail(ENV, f"{root} не похож на корень проекта: нет ни docs/, ни CLAUDE.md") + + rep = Report() + cfg = read_config(root, rep) + check_version(root, cfg, rep) + check_required(root, cfg, rep) + check_stray(root, rep) + check_links(root, rep) + check_placeholders_and_debt(root, rep) + check_capabilities(root, rep) + check_migrations(root, cfg, args.base, rep) + check_tasks(root, rep) + return report(rep) + + +def cmd_version(args: argparse.Namespace) -> int: + root = Path(args.dir).resolve() + cfg = read_config(root, Report()) + got = cfg.get("canon", "не объявлена") + print(f"канон скрипта: {CANON_VERSION}") + print(f"канон проекта: {got}") + return OK + + +def main() -> int: + parser = argparse.ArgumentParser( + prog="docs.py", + description="механическая проверка канона документов проекта", + ) + sub = parser.add_subparsers(dest="cmd", required=True) + + p_check = sub.add_parser("check", help="раскладка, ссылки, версия, сверки с кодом") + p_check.add_argument("--dir", default=".", help="корень проекта (по умолчанию текущий)") + p_check.add_argument("--base", default=None, help="база диффа для сверки миграций") + p_check.set_defaults(func=cmd_check) + + p_ver = sub.add_parser("version", help="версия канона скрипта и проекта") + p_ver.add_argument("--dir", default=".", help="корень проекта") + p_ver.set_defaults(func=cmd_version) + + args = parser.parse_args() + try: + return args.func(args) + except SystemExit: + raise + except Exception as exc: # noqa: BLE001 — последний рубеж, код 4 по словарю + print(f"ВНУТРЕННИЙ СБОЙ: {exc}", file=sys.stderr) + return INTERNAL + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/av-dev-pm/skills/docs/SKILL.md b/av-dev-pm/skills/docs/SKILL.md new file mode 100644 index 0000000..fa9cb19 --- /dev/null +++ b/av-dev-pm/skills/docs/SKILL.md @@ -0,0 +1,146 @@ +--- +name: docs +description: Вести содержимое документов канона по ходу разработки — синк после сделанной задачи с построчным отчётом по каждому документу, заведение ADR промоутом из архивного design.md, запись наблюдения в research, запись дефекта и настройки конвейера в review.md, чистка architecture.md от поведения с маркерами долга. Использовать, когда задача сделана и надо обновить документацию, когда просят завести ADR или записать решение, занести находку о внешних данных, записать проскочивший дефект, разгрузить разросшуюся архитектуру. Раскладку и соответствие канону проверяет скилл canon. +--- + +# Ведение содержимого канона + +Скилл владеет **содержимым** документов канона; раскладкой владеет `canon`. +Определение канона и роли документов — [канон](../canon/references/canon.md), +здесь не пересказывается. + +Главный вызывающий — **шаг синка документации в пайплайне задачи**. Пайплайн +живёт в другом плагине и зовёт этот скилл по имени; проект без пайплайна ведёт +документацию тем же скиллом вручную. + +## Правило, из которого всё следует + +**Принуждённое отрицание.** Синк обязан назвать **каждый** документ канона: либо +чем он обновлён, либо «не требуется, потому что…». Нетронутые группируются одной +строкой с общей причиной. + +Причина, по которой правило именно такое, измерена: у ADR был список триггеров +прозой — и он дал **6 записей на 43 изменения**. Прозаический триггер, который +некому проверить, не срабатывает. Умолчание «не написал» становится отличимым от +«написал, что не требуется», только когда отрицание обязательно. + +Это тот же приём, что «границы покрытия» в отчёте ревью и «пустое называется +пустым» в каноне. + +## Чек-лист синка + +Идёт сверху вниз; каждая строка попадает в доклад. + +| Документ | Обновляется, когда | Проверка | +| --- | --- | --- | +| `openspec/specs/` | всегда при изменении поведения | вливает `opsx:archive` | +| `database.md` | тронуты миграции | `docs.py check --base` | +| `architecture.md` | новый компонент, граница, внешняя зависимость, изменилось окружение | `docs.py`: capability без упоминания | +| `adr/` | дорогой откат, намеренный отказ, пересмотр прежнего | нет — только этот чек-лист | +| `research/` | узнали новое о внешнем формате или данных | нет | +| `security.md` | новый недоверенный вход, токен, путь наружу, сдвиг периметра | нет | +| `conventions/` | находка принята и не специфична для одного места | промоут | +| `review.md` | дефект воспроизведён; сузили или расширили проверку | нет | +| `passport.md` | новый потребитель, сдвиг границы «чем не является» | нет | +| `CLAUDE.md` | изменился инвариант, гейт, запрет, необратимое | нет | + +Пример доклада: + +``` +Синк документации: +- architecture.md — добавлен воркер свёртки, ссылка на capability reindex +- database.md — миграция 00006, таблица bucket +- adr/ — заведён ADR-2026-08-03-ochered-tablicej: отказ от внешней очереди +- research/ — новое о формате не узнано +- passport, security, conventions, review — не требуется: изменение внутреннее +``` + +## ADR — промоут, а не второе сочинение + +Обоснование уже написано: `opsx:propose` кладёт `design.md` в каждый change, и +после архивации он лежит в `openspec/changes/archive//design.md` с разделами +`Context` / `Goals / Non-Goals` / `Decisions` / `Risks / Trade-offs`. + +**ADR цитирует решение оттуда и ссылается на источник.** Не пересказывает и не +сочиняет заново. + +Заводится, когда верно одно из трёх: + +- **дорогой откат** — переделка стоит дороже переписывания одного файла; +- **намеренный отказ** от очевидного подхода — чтобы не переоткрывать «а почему + мы не сделали X»; +- **пересмотр прежнего решения** — тогда у старой записи обязателен статус + `заменено на ADR-…`, а у новой в контексте строка «Заменяет ADR-…». + +Не заводится для рутины и для того, что видно из кода и `git log`. + +Порядок: имя `ADR-ГГГГ-ММ-ДД-slug.md`, дата — когда решение **принято**, слаг +английский; тело по `docs/adr/template.md`; строка в индексе `docs/adr/README.md` +сверху. Активная запись статуса не имеет. + +## Чистка `architecture.md` + +Обзор не держит поведение — его нормативный дом `openspec/specs/`. Раздел, где +поведение осталось, помечается маркером долга: + +``` + +``` + +`docs.py` считает маркеры и печатает числом; **гейт от них не краснеет** — это +долг, а не отказ, иначе постепенный переезд стал бы невозможен. + +Разбирается порциями: раздел вычищается той задачей, которая его касается. +Содержимое не выбрасывается, а переезжает — требования в дельта-спеку change, +обоснование в ADR, обзор остаётся строкой со ссылкой на capability. + +## Запись в `research/` + +Наблюдение о внешнем мире: что реально шлёт источник, чем документация формата +расходится с практикой. **Число — с провенансом**: команда или условия, которыми +получено, чтобы его можно было перепроверить. + +Число без источника проход обязан читать как условие. Число, чей источник по +ссылке не подтвердился, **не переписывается по догадке** — остаётся с пометкой +«расходится с источником: там <что нашли>». Молча подставить «правильное» число +хуже всего: расхождение перестанет быть видно, а причина останется. + +## Запись в `review.md` + +Два раздела с разными сроками жизни, и путать их нельзя. + +**Журнал дефектов.** Запись на каждый воспроизведённый дефект с пометкой +**проскочил / пойман ревью**. Пишется сразу, а не ретроспективно: со временем +теряется не факт, а причина непоймания — единственное, ради чего журнал есть. +Форма: где, симптом, чем воспроизведён, почему не поймали (для проскочивших), +что меняем. Вывод «ничего не меняем, цена поимки выше цены дефекта» — законный +исход. + +**Настройка конвейера.** Типовые узлы; типовые ложноположительные; вопросы к +проходам поимённо с провенансом; недоступно проверке. Последний раздел делится +на «не проверит ни один проход» (принципиальная граница, по факту промаха не +пересматривается) и «перестали проверять сознательно» — этот **пересматривается +первым**, как только что-то проскочило. + +## Промоут в конвенции + +Находка → конвенция → правило линтера → **удаление из прозы**. Процедура +принадлежит конвейеру ревью и живёт в его `references/promote.md`; здесь только +то, что касается документа: + +- формулировка — **проверяемое свойство**, а не совет; +- в прозе остаётся только то, что принципиально не выражается правилом; +- как только правило работает, формулировка из `conventions/<тема>.md` + **удаляется**, а правило попадает в перечень механизированного в + `conventions/README.md` со ссылкой на место механизации. + +Непойманное место механизации означает, что проход по конвенциям будет +добросовестно проверять уже проверенное. + +## Чего этот скилл не делает + +- **Не проверяет раскладку** — это `canon`. +- **Не заводит недостающие документы** — их скелет кладёт `canon adopt` или + `init`. +- **Не сочиняет содержание.** Нечего записать — так и пишется, честной строкой. +- **Не переоформляет документы «заодно»**: правится то, чего коснулась работа. diff --git a/av-dev-pm/skills/init/SKILL.md b/av-dev-pm/skills/init/SKILL.md new file mode 100644 index 0000000..3b0c6b6 --- /dev/null +++ b/av-dev-pm/skills/init/SKILL.md @@ -0,0 +1,92 @@ +--- +name: init +description: Завести новый проект — сессия вопросов и ответов по свободному описанию замысла, из которой рождается первичная документация по канону av-dev: паспорт, CLAUDE.md с инвариантами и командами, модель угроз с периметром, первые цели в плане и скелет остальных документов. Использовать, когда начинают новый проект с нуля, когда есть только текст «что мне нужно и почему» и надо превратить его в рабочую документацию, когда просят провести стартовое интервью по брифу. Проект, где документация уже как-то ведётся, переводит скилл canon. +--- + +# Заведение нового проекта + +Вход — свободный текст «что мне нужно и почему». Выход — канон документов, с +которого дальше работают все остальные скиллы. + +**Определение канона — [канон](../canon/references/canon.md).** Читается до +первого вопроса: интервью идёт по слотам канона, а не по вкусу. + +## Что `init` физически не может произвести + +В новом репозитории **нет кода**, а `architecture.md`, `database.md`, +`conventions/` и `research/` выводятся из него. Их сочинение на старте — это +проектирование вперёд реальности, и оно протухнет раньше первой задачи. + +Поэтому `init` заполняет то, что человек знает **до первой строки кода**: + +| Заполняется | Остаётся скелетом с честной строкой | +| --- | --- | +| `passport.md` | `architecture.md` | +| `CLAUDE.md` | `database.md` | +| `security.md` | `conventions/` | +| `docs/tasks/PLAN.md` — первые цели | `research/`, `adr/` | +| `docs/.pm.json` | `review.md` — журнал пуст, настройка появится с первым ревью | + +Честная строка информативна, а не «TBD»: «архитектуры пока нет: кода нет, +заводится первой задачей». Проход читает её как факт. + +## Порядок интервью — зависимость, а не удобство + +Каждый блок опирается на ответ предыдущего; переставлять нельзя. + +1. **Цель и потребители.** Ради чего это; кто пользуется — список закрытый, и + он определяет, что считать нужным, а что интересным. +2. **Чем это НЕ является и мера успеха.** Граница домена — критерий, по + которому архитектурный проход потом судит о переносе понятия. Мера — по чему + поймём, что удалось. +3. **Периметр и недоверенный вход.** Открыт наружу или контур доверенный; что + приходит извне и каким каналом; что чувствительнее чего. Контур ещё не + развёрнут — назови **оба** периметра, целевой и сегодняшний. +4. **Стек, хранилище, необратимое.** Чем пишем и почему; где данные; что в этом + проекте нельзя откатить — деплой, выкладка наружу, перезапись данных. +5. **Чем краснеет гейт.** Какие проверки обязательны; что красит безусловно; + чего в гейте намеренно не будет и кто тогда это гоняет. +6. **Первые цели.** Направления, а не задачи: три-пять целей линии с + обоснованием порядка прозой. + +### Как вести + +- **Не больше трёх вопросов за итерацию** (`AskUserQuestion`), рекомендация + первым вариантом. Между итерациями применяй уже решённое. +- **Сперва вычитай ответы из брифа.** Вопрос, ответ на который в тексте уже + есть, задавать не надо — покажи своё прочтение и спроси, верно ли. +- **Не выдумывай четыре вещи:** периметр, что необратимо, измеренные числа и + адресата дорогой проверки. Их из замысла не вывести. Не сказано — пиши + «неизвестно» с пометкой, что ждёт ответа. +- **Развилка замысла — человеку, механика — сама.** Имена файлов, слаги, порядок + строк не выносятся. + +## Порядок работы + +1. Прочитай бриф целиком. Выпиши, на какие блоки интервью ответ уже есть. +2. Проведи интервью итерациями по ≤3 вопроса. +3. Заведи `docs/.pm.json` с текущей версией канона. +4. Напиши заполняемые документы. **Бриф переезжает в `passport.md`** и + отдельным файлом не остаётся: два дома для одного замысла разойдутся на + первом же уточнении. +5. Заведи скелет остальных — каждый с честной строкой. +6. Каталог задач и первые цели — **вызови скилл `av-dev-pm:tasks`**: он владеет + форматом целей и задач. +7. `docs.py check` из скилла `canon` — до зелёного в механизируемой части. +8. Покажи человеку, что получилось, и **отдельным списком** — что выведено из + брифа, что предположено, что осталось неизвестным. Правят по этим строкам. + +## Что дальше + +- Содержимое канона по ходу разработки ведёт скилл `docs`. +- Раскладку проверяет `canon check`. +- Первую задачу берёт пайплайн проекта; `architecture.md` и `conventions/` + наполняются его шагом синка, а не заранее. + +## Чего этот скилл не делает + +- **Не проектирует систему.** Архитектура выводится из кода, а не наоборот. +- **Не пишет код** и не заводит сборку. +- **Не переводит существующий проект** — это `canon adopt`. Признак: в + репозитории уже есть документация или беклог в какой-то раскладке. +- **Не решает за человека**, что важно: цель, границы и периметр — его ответы. diff --git a/av-dev-tasks/skills/session/SKILL.md b/av-dev-pm/skills/session/SKILL.md similarity index 84% rename from av-dev-tasks/skills/session/SKILL.md rename to av-dev-pm/skills/session/SKILL.md index e4116bd..7fd0d78 100644 --- a/av-dev-tasks/skills/session/SKILL.md +++ b/av-dev-pm/skills/session/SKILL.md @@ -157,8 +157,21 @@ python3 $tk reopen <слаг> --dir D --reason … # приёмка не со ## Стимулы, которые процесс создаёт -Правило, которое можно обойти в свою пользу, будет обойдено. Известные обходы и -защиты: +Правило, которое можно обойти в свою пользу, будет обойдено. + +**Приёмщик и исполнитель здесь совпадают, и это надо назвать вслух.** Задачу +закрывает и двигает по индексам агент-оркестратор — тот же, кто её и сделал. +Прежде границу держала механика: моста между плагинами не было, и закрыть задачу +пайплайн физически не мог. Теперь мост есть, и защита у трёх обходов ниже — +**только текстовая**. Опоры, которые остались настоящими: + +- **отчёт триажа** в `openspec/changes//review/` — независимый артефакт, + написанный ревью, а не исполнителем; по нему сверяют состав прогона и урожай; +- **`SPRINT.md` под git** — `git log -p` показывает, что и когда было закрыто; +- **`reopen <слаг> --reason`** — закрытие не окончательно. Приёмка человеком на + сессии его отменяет, и это штатная операция, а не скандал. + +Известные обходы: - **Скрыть блокер** — он останавливает всё и выглядит как провал исполнителя. Защита: тест про остаток плюс прямая запись, что **объявление блокера @@ -166,13 +179,16 @@ python3 $tk reopen <слаг> --dir D --reason … # приёмка не со - **Не записать вопрос** на задаче-кандидате, чтобы не вычеркнуть её из ближайшего набора. Защита: вопросы кандидатов разбираются на той же сессии **вне очереди порции**. -- **Занизить критерии приёмки**, раз они пол. Защита: расхождение критериев с - сутью — дефект критериев, правит их приёмщик, а он **не исполнитель**. -- **Сжать задачу до остатка** и отчитаться «сделана». Защита: пол для остатка — - польза, названная в хуке. +- **Занизить критерии приёмки**, раз они пол. Защита ослаблена: правит их тот же, + кто по ним отчитывается. Остаётся требование, что расхождение критериев с + сутью — **дефект критериев, о котором сообщают, а не молча дорабатывают**, и + переоценка на сессии, где критерии видит человек. +- **Сжать задачу до остатка** и отчитаться «сделана». Защита ослаблена там же. + Пол для остатка — польза, названная в хуке; проверяет его человек при приёмке, + и `reopen` — его инструмент. - **Занизить урожай** — не заводить найденное по ходу. Защита: поимённая сверка - отчётов ревью со списком заведённого, составленным **не отчитывающимся**: - каждая отложенная находка имеет либо слаг, либо строку «не заведена: причина». + со **сохранённым отчётом триажа**, а не с прозой исполнителя. Каждая + отложенная находка имеет либо слаг, либо строку «не заведена: причина». Нулевой урожай при непустом отчёте виден сразу. Стимулы внутри пайплайна задачи (занизить требования к проверке, пропустить @@ -180,27 +196,24 @@ python3 $tk reopen <слаг> --dir D --reason … # приёмка не со ## Слоты проекта -Сессия не знает ни языка, ни сборки, ни CI. Проект **обязан дописать в -`CLAUDE.md`**: +Сессия не знает ни языка, ни сборки, ни CI. Часть проектного отвечает +[канон](../canon/references/canon.md) структурой: разбор процесса (шаг 2) живёт +в `docs/review.md`, оракулы и «чем краснеет безусловно» — в семантике гейта в +`CLAUDE.md`. Остальное проект **дописывает в `CLAUDE.md`**: 1. **Пайплайн задачи** — чем задача выполняется и что входит в его определение готовности. Сессия требует только форму: пайплайн пройден + критерии приёмки проверены поимённо. 2. **Общий станок** — какая проверка, покраснев, врывается в замороженный спринт. -3. **Необратимое** — что спрашивается у человека всегда. -4. **Где живёт разбор процесса** (шаг 2): журнал промахов конвейера, ADR или - раздел документации. Нет такого места — шаг 2 производит его первым же - заходом, иначе выводы сессии живут один контекст. -5. **Как критерии приёмки переживают удаление файла задачи** — файл удаляется +3. **Необратимое** — что спрашивается у человека всегда (тот же слот, что у + скилла `tasks`; дом один). +4. **Как критерии приёмки переживают удаление файла задачи** — файл удаляется при закрытии, поэтому критерии копируются туда, где их увидит приёмщик (предложение об изменении, описание ветки, тело коммита). Куда именно — решает проект. -6. **Ориентир по размеру спринта**, если он замерялся. Умолчание — 5–8 задач, и +5. **Ориентир по размеру спринта**, если он замерялся. Умолчание — 5–8 задач, и это **ориентир, а не закон**. -7. **Команда учёта задач** — готовая строка вызова `tasks.py` (слот скилла - `tasks`). Ею владелец спринта закрывает задачи и заводит урожай; чужой - контекст сам путь к плагину не знает и знать не должен. Числа проекта (сколько задач в спринте, сколько времени на задачу, каков прирост беклога) — предмет шага 2, а не константы этого скилла. diff --git a/av-dev-tasks/skills/session/references/cadence.md b/av-dev-pm/skills/session/references/cadence.md similarity index 97% rename from av-dev-tasks/skills/session/references/cadence.md rename to av-dev-pm/skills/session/references/cadence.md index e6f3313..9542a04 100644 --- a/av-dev-tasks/skills/session/references/cadence.md +++ b/av-dev-pm/skills/session/references/cadence.md @@ -44,9 +44,10 @@ их не пересматривает конкретный шаг. **Артефакт обязателен.** Вывод, оставшийся в контексте сессии, не существует: -следующая сессия его не увидит. Куда он пишется — журнал промахов, ADR, раздел -документации — называет `CLAUDE.md` проекта; нет такого места, значит первый -разбор его и заводит. +следующая сессия его не увидит. Дом у него один и известен из канона — +**`docs/review.md`**: вывод про конвейер и про то, что перестали проверять, идёт +в раздел настройки, вывод про воспроизведённый дефект — в журнал. Решение с +долгим следом — в `docs/adr/`. Отдельным ритуалом ретроспектива не выделяется: процесс личный, синхронизировать некого. diff --git a/av-dev-tasks/skills/session/references/sprint.md b/av-dev-pm/skills/session/references/sprint.md similarity index 100% rename from av-dev-tasks/skills/session/references/sprint.md rename to av-dev-pm/skills/session/references/sprint.md diff --git a/av-dev-tasks/skills/tasks/SKILL.md b/av-dev-pm/skills/tasks/SKILL.md similarity index 83% rename from av-dev-tasks/skills/tasks/SKILL.md rename to av-dev-pm/skills/tasks/SKILL.md index a0af981..a765461 100644 --- a/av-dev-tasks/skills/tasks/SKILL.md +++ b/av-dev-pm/skills/tasks/SKILL.md @@ -41,8 +41,12 @@ description: Ведение задач и целей как каталога mar ## Раскладка +Каталог задач — **`docs/tasks`, жёстко**: это часть +[канона документов](../canon/references/canon.md), и подгоняется под него +проект, а не наоборот. + ``` -/ по умолчанию docs/tasks, путь настраивается +docs/tasks/ items/ задачи и цели файлами, .md, слаги английские PLAN.md оглавление целей: линия (упорядоченная) и кусты BACKLOG.md что можно взять — только задачи, целей здесь нет @@ -70,7 +74,7 @@ description: Ведение задач и целей как каталога mar **У сделанной задачи записи не остаётся** — файл и строка удаляются (`close --implemented`). Ей хватает коммита и документации проекта; вторая запись была бы вторым домом для того же факта. Вопрос «что было в спринте N» отвечается -даром: `SPRINT.md` лежит под git, `git log -p /SPRINT.md` отдаёт историю +даром: `SPRINT.md` лежит под git, `git log -p docs/tasks/SPRINT.md` отдаёт историю всех наборов без отдельного журнала. ## Цели @@ -100,9 +104,9 @@ description: Ведение задач и целей как каталога mar ## Инструмент (`tasks.py`) -Пусть `tk="$CLAUDE_PLUGIN_ROOT/skills/tasks/scripts/tasks.py"`, а `D` — каталог -задач проекта (см. «Переносимость»; `--dir` опускается только если каталог -лежит в умолчаниях под текущим каталогом). +Пусть `tk="$CLAUDE_PLUGIN_ROOT/skills/tasks/scripts/tasks.py"`, а `D` — +`docs/tasks` от корня проекта. `--dir` стоит в примерах намеренно: вызов из +подкаталога — обычное дело. ``` python3 $tk check --dir D # согласованность индексов + здоровье @@ -116,7 +120,7 @@ python3 $tk close S --dir D --implemented # просто удалить (ре python3 $tk reopen S --dir D --reason R # вернуть закрытую: приёмка не сошлась python3 $tk sprint start --goal S --dir D | take S… | drop S… --reason R | close [--dissolve --reason R] python3 $tk init --dir D [--sections …] [--plan-sections …] [--items …] [--backlog …] … -python3 $tk adopt scan --from … | apply --plan … # разовая адаптация чужого репозитория, скилл adopt +python3 $tk adopt scan --from … | apply --plan … # разовая адаптация, references/adopt.md ``` **Коды выхода — единый словарь; на нём ветвятся скиллы, а не на тексте вывода:** @@ -126,7 +130,7 @@ python3 $tk adopt scan --from … | apply --plan … # разовая адап | 0 | сошлось / сделано | дальше по сценарию | | 1 | **только `check`:** найден дрейф индексов и файлов | `check --fix`, остаток разобрать | | 2 | ошибка употребления: аргументы или нарушенное правило | читать сообщение, это отказ по существу | -| 3 | окружение: каталог не найден, конфиг битый или мимо диска | чинится путём или `.tasks.json`, повтор не поможет | +| 3 | окружение: каталог не найден, конфиг битый или мимо диска | чинится путём или `docs/.pm.json`, повтор не поможет | | 4 | внутренний сбой | дефект скрипта, доложить | Различать 1 и 3 обязательно: «дрейф в беклоге» — рабочая ситуация, «каталога @@ -220,9 +224,12 @@ python3 $tk adopt scan --from … | apply --plan … # разовая адап ### Прийти в репозиторий, где задачи уже как-то ведутся Разовая операция: вывести каталог задач из старой раскладки беклога, `TODO.md`, -заметок или списка шагов в плане — скилл `adopt`. Сюда же относится -переименование транслитных слагов в английские: оно делается **одним проходом -вместе с починкой перекрёстных ссылок**, а не по одному слагу. +заметок или списка шагов в плане — [references/adopt.md](references/adopt.md). +Сюда же относится переименование транслитных слагов в английские: оно делается +**одним проходом вместе с починкой перекрёстных ссылок**, а не по одному слагу. + +Если переводить надо не только задачи, а весь `docs/` — это скилл +`av-dev-pm:canon`, и он зовёт этот сценарий сам на своём шаге. ### Декомпозиция и штурм идеи @@ -256,66 +263,47 @@ python3 $tk adopt scan --from … | apply --plan … # разовая адап Скилл независим от **языка программирования, сборки, CI и трекера**: он ничего не знает ни про Go, ни про npm, ни про конкретный багтрекер — задачи для него просто каталог markdown. Текст задач — русский (язык документации проекта); -зашита только латиница слага. +зашита только латиница слага. OpenSpec ему тоже не нужен. -- **Каталог задач** ищется цепочкой: `--dir` → **указатель в `CLAUDE.md` - проекта** (его читаешь ты и передаёшь `--dir`; скрипт чужую документацию не - разбирает) → `.tasks.json` вверх от текущего каталога → умолчания - (`docs/tasks`, `tasks`, `doc/tasks`) вверх от текущего каталога, до корня - репозитория. Не нашлось — код 3 и вопрос человеку, а не догадка: `init` - заводит каталог **только** когда проект действительно новый. - **В примерах `--dir` стоит намеренно:** каталог вне умолчаний иначе не - находится, а вызов из подкаталога — обычное дело. +- **Каталог задач — `docs/tasks`, жёстко.** Цепочки разрешения нет: раскладка + канона одинакова во всех проектах, и искать больше нечего. Каталога нет — код + 3 и вопрос человеку; `init` заводит его **только** когда проект действительно + новый, а перевод чужой раскладки делает `av-dev-pm:canon`. +- **Настройки живут в `docs/.pm.json`**, ключ `tasks`: секции беклога и имена + индексов, если они отличаются от умолчания. Один конфиг на весь канон, а не по + одному на каталог. - **Секции беклога** берутся из заголовков `##` индекса как есть; их количество и названия — дело проекта (умолчание `ядро` / `инфра`). -- **Имена индексов и подкаталога** — параметры `init`, живут в - `/.tasks.json`. Ничего не зашито именем файла. ### Вызов из другого плагина -`$CLAUDE_PLUGIN_ROOT` раскрывается **только внутри своего плагина**: чужой -контекст — пайплайн задачи, конвейер ревью, любой другой скилл — до `tasks.py` -по этой переменной не дотянется. Поэтому контракт такой: +`$CLAUDE_PLUGIN_ROOT` раскрывается **только внутри своего плагина**: пайплайн +задачи, конвейер ревью и любой другой чужой контекст до `tasks.py` по этой +переменной не дотянутся. Мост — **вызов скилла через пространство имён**, а не +путь: -> **Проект называет команду учёта задач в своём `CLAUDE.md`** — целиком, готовой -> к запуску строкой (слот 6 ниже). Вызывающий берёт её оттуда. Слота нет — -> вызывающий **не выдумывает путь и не правит индекс руками**, а сообщает в -> докладе, что закрытие/заведение остаётся за владельцем задач. +> Чужой контекст зовёт `Skill av-dev-pm:tasks` и называет, что нужно сделать +> («закрой задачу `<слаг>`, реализована»). Скилл разрешает свой +> `$CLAUDE_PLUGIN_ROOT` сам. Путь наружу не выносится вовсе. -Так вызывающему не нужно знать ни про плагин, ни про его расположение: он знает -проект, а проект знает команду. +Плагина в проекте нет — вызов не разрешится, и вызывающий **не выдумывает путь и +не правит индекс руками**, а сообщает в докладе, что учёт задач остаётся за +владельцем. ## Слоты проекта -Скилл не знает ни языка программирования, ни сборки, ни CI, ни трекера — задачи -для него просто каталог markdown. Всё проектное живёт в `CLAUDE.md` проекта, и -**проект обязан дописать туда**: +Почти всё, что скиллу нужно знать о проекте, отвечает канон структурой: куда +переезжает суть реализованной задачи — `openspec/specs`, `adr/`, архив change; +какие в проекте оракулы — семантика гейта в `CLAUDE.md`. Отдельными слотами +остаётся то, чего из раскладки не вывести. **Проект дописывает в `CLAUDE.md`**: -1. **Путь каталога задач**, если он не `docs/tasks`, и **секции беклога** — по - умолчанию `ядро` / `инфра`; граница между ними режется по существу работы, а - не по её поводу. Имена индексов и подкаталога, если они другие, задаются - `init` и живут в `/.tasks.json`. -2. **Что такое «сделана»** — чем задача выполняется (пайплайн проекта) и что +1. **Что такое «сделана»** — чем задача выполняется (пайплайн проекта) и что входит в его определение готовности. Скилл требует лишь **форму**: пайплайн проекта пройден + критерии приёмки проверены поимённо. -3. **Куда переезжает суть реализованной задачи** — спеки, ADR, архив изменений: - без этого не проверить, что задача закрыта не коммитом, а решением. -4. **Что считается необратимым** и потому спрашивается у человека всегда +2. **Что считается необратимым** и потому спрашивается у человека всегда (деплой, выкладка наружу, удаление или перезапись данных). -5. **Оракулы, которые в проекте вообще есть** — чем проверяется критерий - приёмки: тест, команда, прогон на реальных данных, глазами по логу. -6. **Команда учёта задач** — готовая строка, которой чужой контекст зовёт - `tasks.py`, потому что путь к плагину ему неизвестен. Например: - ``` - Команда учёта задач: python3 ~/.claude/plugins/marketplaces/av-dev-skills/\ - av-dev-tasks/skills/tasks/scripts/tasks.py --dir docs/tasks - ``` - - Слот заполняется один раз при подключении плагина. Он же отвечает на вопрос - «кто закрывает задачу»: команду знает проект, зовёт её владелец спринта. - -Ничего из этого скилл не угадывает: не нашёл — спрашивает пользователя, а не +Ни того ни другого скилл не угадывает: не нашёл — спрашивает пользователя, а не подставляет умолчание. ## Общее для всех сценариев diff --git a/av-dev-tasks/skills/adopt/SKILL.md b/av-dev-pm/skills/tasks/references/adopt.md similarity index 82% rename from av-dev-tasks/skills/adopt/SKILL.md rename to av-dev-pm/skills/tasks/references/adopt.md index 92c6dae..5d3fa02 100644 --- a/av-dev-tasks/skills/adopt/SKILL.md +++ b/av-dev-pm/skills/tasks/references/adopt.md @@ -1,13 +1,13 @@ ---- -name: adopt -description: Прийти в чужой репозиторий и вывести каталог задач из того, что там уже есть — старая раскладка беклога (README-индекс, CLOSED-кладбище, транслитные слаги), TODO.md, россыпь заметок, раздел «планы» в README, список шагов в плане проекта. Сперва карта находок и целей человеку, запись только после подтверждения; слаги переименовываются в английские вместе с починкой перекрёстных ссылок. Использовать, когда просят перевести проект на этот формат задач, перенести беклог, адаптировать существующие заметки под цели и спринты. Разовая операция: дальше проект ведут скиллы tasks и session. ---- +# Адаптация каталога задач -# Адаптация чужого репозитория +Проект, где задачи уже как-то ведутся, и из имеющегося материала **выводится** +заполненный каталог задач: цели, задачи, кладбище, индексы. Операция разовая — +после неё проект живёт скиллами `tasks` и `session`. -Плагин приходит в проект, где задачи уже как-то ведутся, и **выводит** из -имеющегося материала заполненный каталог задач: цели, задачи, кладбище, индексы. -Операция разовая — после неё проект живёт скиллами `tasks` и `session`. +**Это часть приведения проекта к канону.** Раскладку `docs/` целиком ведёт скилл +`av-dev-pm:canon`; он же зовёт этот сценарий на шаге «каталог задач», потому что +форматом задач владеет `tasks`, а не `canon`. Отдельно сценарий вызывается, +когда переводить надо **только** задачи. Вход какой угодно: старая раскладка `av-dev-backlog` (индекс `README.md`, кладбище `CLOSED.md`, приоритеты секциями, транслитные слаги, файлы рядом с @@ -61,10 +61,10 @@ python3 $tk adopt apply --plan tasks-adopt-plan.json \ ## Порядок -1. **Осмотрись.** Где лежат задачи, план, заметки; читается ли `CLAUDE.md` - проекта — там может быть указатель на каталог. Секции беклога проекта - (`--sections`) — по умолчанию `ядро,инфра`; если у проекта деление другое по - существу, оно называется здесь, а не подгоняется под умолчание. +1. **Осмотрись.** Где лежат задачи, план, заметки. Каталог задач по канону — + всегда `docs/tasks`. Секции беклога (`--sections`) — по умолчанию + `ядро,инфра`; если у проекта деление другое по существу, оно называется + здесь, а не подгоняется под умолчание, и уезжает в `docs/.pm.json`. 2. **`adopt scan`** по всем источникам разом. Один прогон, одна карта: два прохода дадут два несогласованных состояния. 3. **Заполни карту**: `slug` (английский), `section`, `goal` у каждой записи; diff --git a/av-dev-tasks/skills/tasks/references/from-review.md b/av-dev-pm/skills/tasks/references/from-review.md similarity index 100% rename from av-dev-tasks/skills/tasks/references/from-review.md rename to av-dev-pm/skills/tasks/references/from-review.md diff --git a/av-dev-tasks/skills/tasks/references/split.md b/av-dev-pm/skills/tasks/references/split.md similarity index 100% rename from av-dev-tasks/skills/tasks/references/split.md rename to av-dev-pm/skills/tasks/references/split.md diff --git a/av-dev-tasks/skills/tasks/references/task-format.md b/av-dev-pm/skills/tasks/references/task-format.md similarity index 100% rename from av-dev-tasks/skills/tasks/references/task-format.md rename to av-dev-pm/skills/tasks/references/task-format.md diff --git a/av-dev-tasks/skills/tasks/scripts/tasks.py b/av-dev-pm/skills/tasks/scripts/tasks.py similarity index 100% rename from av-dev-tasks/skills/tasks/scripts/tasks.py rename to av-dev-pm/skills/tasks/scripts/tasks.py diff --git a/av-dev-tasks/.claude-plugin/plugin.json b/av-dev-tasks/.claude-plugin/plugin.json deleted file mode 100644 index 3b6115a..0000000 --- a/av-dev-tasks/.claude-plugin/plugin.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "av-dev-tasks", - "description": "Управление задачами как каталогом markdown-файлов: цели вместо приоритетов, спринт под одну цель с заморозкой набора, вопросы и блокеры, каденция «разбор — переоценка — набор». Заведение задач из диалога и из находок ревью, декомпозиция, штурм идей, разовая адаптация чужого репозитория под этот формат. Не выполняет задачи — этим занимается пайплайн проекта.", - "author": { - "name": "Anton Vakhrushev", - "email": "anwinged@gmail.com" - } -}