av-dev-pm: плагин переименован, заведены канон документов и скиллы init/canon/docs
- av-dev-tasks → av-dev-pm; канон определён единственным reference-файлом, который читают все три новых скилла - canon: check/adopt/upgrade плюс docs.py — раскладка, битые ссылки, версия, маркеры долга, сверки миграций и capability с документацией - tasks и session: путь docs/tasks жёсткий, конфиг переехал в docs/.pm.json, слот «Команда учёта задач» убран в пользу вызова скилла, раздел «Стимулы» переписан под совпавших приёмщика и исполнителя
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "av-dev-pm",
|
||||
"description": "Управление продуктом: канон документов проекта (паспорт, архитектура, схема БД, безопасность, конвенции, разведка, ADR, журнал ревью), задачи и цели вместо приоритетов, спринт под одну цель с заморозкой набора, старт нового проекта интервью по брифу и приведение существующего к канону. Не выполняет задачи — этим занимается пайплайн проекта.",
|
||||
"author": {
|
||||
"name": "Anton Vakhrushev",
|
||||
"email": "anwinged@gmail.com"
|
||||
}
|
||||
}
|
||||
@@ -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 <rev>] # раскладка, ссылки, версия, сверки
|
||||
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/<capability>/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`: код выхода и число пунктов дрейфа.
|
||||
- Что перенесено: файл → дом, числом и поимённо для спорного.
|
||||
- **Удалённые дубли** — с указанием, против какой спеки сверялся каждый.
|
||||
- **Не разложилось** — поимённо, с причиной.
|
||||
- Переходное состояние числами: честных строк, маркеров долга, задач без
|
||||
критериев.
|
||||
- **Граница покрытия**: что проверила машина, что судил ты, чего не смотрел
|
||||
никто.
|
||||
@@ -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/<capability>/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` вливает дельты; второй дом синхронизировать руками
|
||||
невозможно, и он разойдётся.
|
||||
|
||||
Раздел, ещё не разнесённый при переезде, помечается маркером долга:
|
||||
|
||||
```
|
||||
<!-- канон: поведение → openspec/specs/<capability> -->
|
||||
```
|
||||
|
||||
`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/<id>/design.md`.
|
||||
|
||||
Заводится, когда верно одно из трёх:
|
||||
|
||||
- **дорогой откат** — переделка стоит дороже переписывания одного файла;
|
||||
- **намеренный отказ** от очевидного подхода;
|
||||
- **пересмотр прежнего решения** — тогда у старой записи обязателен статус
|
||||
«заменено на».
|
||||
|
||||
Не заводится для рутины и для того, что видно из кода и `git log`.
|
||||
|
||||
Записи неизменяемы: передумали — заводится новая, старая получает статус.
|
||||
Активная запись статуса не имеет.
|
||||
|
||||
### `review.md`
|
||||
|
||||
Два раздела с разными сроками жизни.
|
||||
|
||||
**Настройка конвейера под проект:** типовые узлы (рода узлов и 3–5 проверяемых
|
||||
свойств к каждому); типовые ложноположительные — находки, которые здесь выглядят
|
||||
убедительно и всегда неверны; вопросы к проходам поимённо с провенансом;
|
||||
недоступно проверке — два подраздела, «не проверит ни один проход»
|
||||
(принципиальная граница, по факту промаха не пересматривается) и «перестали
|
||||
проверять сознательно» (пересматривается первым).
|
||||
|
||||
**Журнал дефектов:** запись на каждый воспроизведённый дефект с пометкой
|
||||
**проскочил / пойман ревью**. Проскочившие — эвал-сет для калибровки конвейера,
|
||||
выборка по пометке. Пойманные с оракулом — лучшая опора для прохода: проектные,
|
||||
воспроизводимые, однажды оказавшиеся правдой.
|
||||
|
||||
### `CLAUDE.md`
|
||||
|
||||
Что это и стек; **инварианты с severity рядом с формулировкой** — по ним
|
||||
проходы присваивают `critical`, поэтому severity стоит здесь, а не выводится
|
||||
каждым проходом заново; команды; **семантика гейта** — чем краснеет безусловно и
|
||||
почему, где логи, что означает исход, чего в гейте намеренно нет, **кто и когда
|
||||
обязан гонять дорогое вне гейта**; что запускать запрещено, с путями; что
|
||||
считается необратимым; общий станок, врывающийся в замороженный спринт; ориентир
|
||||
по размеру спринта.
|
||||
|
||||
### `openspec/config.yaml`
|
||||
|
||||
**Только нужды генерации артефактов** — язык, правила именования capability,
|
||||
придирки валидатора RFC 2119 — плюс ссылки на документы канона. Правило ревью,
|
||||
пересказ конвенций и инварианты сюда не пишутся: у них есть свои дома, и второй
|
||||
дом разойдётся на первой же правке.
|
||||
|
||||
## Правило единственного дома
|
||||
|
||||
Факт живёт ровно в одном файле; остальные ссылаются. Карта на случай спора:
|
||||
|
||||
| Факт | Дом |
|
||||
| --- | --- |
|
||||
| поведение системы | `openspec/specs/<capability>/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>/.tasks.json`: **один конфиг на весь канон, а не по одному на каталог**.
|
||||
|
||||
Ключей будет больше по мере роста проверок; неизвестный ключ `docs.py`
|
||||
игнорирует, отсутствующий — считает «проверка неприменима» и говорит об этом
|
||||
строкой, а не молчит.
|
||||
@@ -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` в гейт проекта.
|
||||
@@ -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/<id>/design.md`.
|
||||
|
||||
## Когда заводить
|
||||
|
||||
Верно одно из трёх:
|
||||
|
||||
- **дорогой откат** — переделка стоит дороже переписывания одного файла;
|
||||
- **намеренный отказ** от очевидного подхода;
|
||||
- **пересмотр прежнего решения** — тогда у старой записи обязателен статус.
|
||||
|
||||
Не заводить для рутины и того, что видно из кода и `git log`.
|
||||
|
||||
## Соглашения
|
||||
|
||||
- Имя файла — `ADR-ГГГГ-ММ-ДД-slug.md`, слаг английский, дата — когда решение
|
||||
реально принято.
|
||||
- Записи неизменяемы: передумали — новая запись, старой ставится статус.
|
||||
- Активная запись статуса не имеет. Значений два: `заменено на ADR-…` и
|
||||
`устарело`.
|
||||
|
||||
## Записи
|
||||
|
||||
Новые сверху.
|
||||
|
||||
| Дата | Запись | Статус |
|
||||
| --- | --- | --- |
|
||||
```
|
||||
|
||||
## `docs/adr/template.md`
|
||||
|
||||
```markdown
|
||||
# Краткий заголовок решения
|
||||
|
||||
- Дата: ГГГГ-ММ-ДД
|
||||
- Источник: openspec/changes/archive/<id>/design.md
|
||||
|
||||
## Решение
|
||||
|
||||
Что именно решено — одной фразой.
|
||||
|
||||
## Почему
|
||||
|
||||
Намерение и причина. Цитата из источника, а не пересказ. Пиши так, чтобы через
|
||||
год было понятно без чтения переписки.
|
||||
|
||||
## Последствия
|
||||
|
||||
- `+` что стало лучше.
|
||||
- `−` чем платим: ограничения, риски, нагрузка на поддержку.
|
||||
```
|
||||
|
||||
## `docs/review.md`
|
||||
|
||||
```markdown
|
||||
# Ревью: настройка и журнал
|
||||
|
||||
## Как настроен конвейер
|
||||
|
||||
### Типовые узлы
|
||||
|
||||
Рода узлов проекта и 3–5 проверяемых свойств к каждому. Рода, а не инвентарь
|
||||
пакетов: род, который проект задумал, но ещё не написал, включать полезно.
|
||||
|
||||
### Типовые ложноположительные
|
||||
|
||||
Находки, которые здесь выглядят убедительно и всегда неверны. Каждая — с одной
|
||||
строкой «почему здесь это не дефект».
|
||||
|
||||
### Вопросы к проходам
|
||||
|
||||
Форма: `<имя прохода>: <вопрос> (<провенанс>)`. Главный источник — журнал ниже.
|
||||
|
||||
### Недоступно проверке
|
||||
|
||||
**Не проверит ни один проход** — принципиальная граница; по факту промаха не
|
||||
пересматривается.
|
||||
|
||||
**Перестали проверять сознательно** — что, когда и почему, со ссылкой на запись
|
||||
журнала. Пересматривается **первым**, как только что-то проскочило.
|
||||
|
||||
## Журнал дефектов
|
||||
|
||||
Запись на каждый воспроизведённый дефект, сразу, а не ретроспективно: со
|
||||
временем теряется не факт, а причина непоймания.
|
||||
|
||||
Форма:
|
||||
|
||||
## ГГГГ-ММ-ДД — краткое последствие [проскочил|пойман]
|
||||
|
||||
- **Где:** файл:строка
|
||||
- **Симптом:** как обнаружилось
|
||||
- **Чем воспроизведён:** тест, команда, замер
|
||||
- **Почему не поймали:** только для проскочивших
|
||||
- **Что меняем:** правило прохода, шаг гейта, конвенция — либо «ничего, цена
|
||||
поимки выше цены дефекта»
|
||||
```
|
||||
|
||||
Новый проект: «Дефектов пока не было. Настройка конвейера появится с первым
|
||||
ревью.»
|
||||
|
||||
## `docs/.pm.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"canon": 1
|
||||
}
|
||||
```
|
||||
|
||||
Плюс `"migrations": "<путь>"`, если есть БД, и `"tasks": {"sections": [...]}`,
|
||||
если секции беклога отличаются от умолчания.
|
||||
@@ -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"<!--\s*канон:\s*(.+?)\s*-->")
|
||||
PLACEHOLDER = re.compile(r"<!--\s*заполнить:\s*(.+?)\s*-->")
|
||||
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())
|
||||
@@ -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/<id>/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/`. Раздел, где
|
||||
поведение осталось, помечается маркером долга:
|
||||
|
||||
```
|
||||
<!-- канон: поведение → openspec/specs/<capability> -->
|
||||
```
|
||||
|
||||
`docs.py` считает маркеры и печатает числом; **гейт от них не краснеет** — это
|
||||
долг, а не отказ, иначе постепенный переезд стал бы невозможен.
|
||||
|
||||
Разбирается порциями: раздел вычищается той задачей, которая его касается.
|
||||
Содержимое не выбрасывается, а переезжает — требования в дельта-спеку change,
|
||||
обоснование в ADR, обзор остаётся строкой со ссылкой на capability.
|
||||
|
||||
## Запись в `research/`
|
||||
|
||||
Наблюдение о внешнем мире: что реально шлёт источник, чем документация формата
|
||||
расходится с практикой. **Число — с провенансом**: команда или условия, которыми
|
||||
получено, чтобы его можно было перепроверить.
|
||||
|
||||
Число без источника проход обязан читать как условие. Число, чей источник по
|
||||
ссылке не подтвердился, **не переписывается по догадке** — остаётся с пометкой
|
||||
«расходится с источником: там <что нашли>». Молча подставить «правильное» число
|
||||
хуже всего: расхождение перестанет быть видно, а причина останется.
|
||||
|
||||
## Запись в `review.md`
|
||||
|
||||
Два раздела с разными сроками жизни, и путать их нельзя.
|
||||
|
||||
**Журнал дефектов.** Запись на каждый воспроизведённый дефект с пометкой
|
||||
**проскочил / пойман ревью**. Пишется сразу, а не ретроспективно: со временем
|
||||
теряется не факт, а причина непоймания — единственное, ради чего журнал есть.
|
||||
Форма: где, симптом, чем воспроизведён, почему не поймали (для проскочивших),
|
||||
что меняем. Вывод «ничего не меняем, цена поимки выше цены дефекта» — законный
|
||||
исход.
|
||||
|
||||
**Настройка конвейера.** Типовые узлы; типовые ложноположительные; вопросы к
|
||||
проходам поимённо с провенансом; недоступно проверке. Последний раздел делится
|
||||
на «не проверит ни один проход» (принципиальная граница, по факту промаха не
|
||||
пересматривается) и «перестали проверять сознательно» — этот **пересматривается
|
||||
первым**, как только что-то проскочило.
|
||||
|
||||
## Промоут в конвенции
|
||||
|
||||
Находка → конвенция → правило линтера → **удаление из прозы**. Процедура
|
||||
принадлежит конвейеру ревью и живёт в его `references/promote.md`; здесь только
|
||||
то, что касается документа:
|
||||
|
||||
- формулировка — **проверяемое свойство**, а не совет;
|
||||
- в прозе остаётся только то, что принципиально не выражается правилом;
|
||||
- как только правило работает, формулировка из `conventions/<тема>.md`
|
||||
**удаляется**, а правило попадает в перечень механизированного в
|
||||
`conventions/README.md` со ссылкой на место механизации.
|
||||
|
||||
Непойманное место механизации означает, что проход по конвенциям будет
|
||||
добросовестно проверять уже проверенное.
|
||||
|
||||
## Чего этот скилл не делает
|
||||
|
||||
- **Не проверяет раскладку** — это `canon`.
|
||||
- **Не заводит недостающие документы** — их скелет кладёт `canon adopt` или
|
||||
`init`.
|
||||
- **Не сочиняет содержание.** Нечего записать — так и пишется, честной строкой.
|
||||
- **Не переоформляет документы «заодно»**: правится то, чего коснулась работа.
|
||||
@@ -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`. Признак: в
|
||||
репозитории уже есть документация или беклог в какой-то раскладке.
|
||||
- **Не решает за человека**, что важно: цель, границы и периметр — его ответы.
|
||||
@@ -0,0 +1,225 @@
|
||||
---
|
||||
name: session
|
||||
description: Ритуал между спринтами и ведение самого спринта: разбор накопившихся вопросов, разбор прошедшего спринта про процесс, переоценка задач порциями, выбор цели и набор нового спринта с заморозкой. Плюс правила по ходу спринта — что врывается в замороженный набор, чем вопрос отличается от блокера, когда задача выходит из спринта, что считается сделанным и что идёт в доклад. Использовать, когда просят закрыть или начать спринт, собрать набор, разобрать вопросы, провести груминг/переоценку/ретроспективу, решить «что делать дальше» или доложить итоги. Формат и содержимое задач — скилл tasks.
|
||||
---
|
||||
|
||||
# Сессия между спринтами
|
||||
|
||||
Работа идёт спринтами: **набор задач под одну цель, замороженный до конца
|
||||
спринта**. Между спринтами — одна сессия из четырёх шагов. Этот скилл владеет
|
||||
**ритуалом**: как сессия проводится и как спринт ведётся. Форматом и содержимым
|
||||
задач владеет скилл `tasks`, выполнением задачи — пайплайн проекта.
|
||||
|
||||
## Почему не Scrum
|
||||
|
||||
Терминология близка — спринт, груминг, определение готовности, ретроспектива, —
|
||||
и это удобно: не нужно изобретать слова. Но добрая половина Scrum существует
|
||||
ради синхронизации людей, которых здесь нет.
|
||||
|
||||
**Не берём:** тайм-бокс (спринт ограничен объёмом, а не временем), velocity и
|
||||
оценки в очках, ежедневный стендап (стендап — это и есть диалог), планирование
|
||||
отдельно от груминга (владелец беклога один), роль скрам-мастера.
|
||||
|
||||
**Берём:** цель спринта, заморозку набора, определение готовности, груминг —
|
||||
каждое потому, что снимает решение, которое иначе принимается заново каждый раз.
|
||||
**Ретроспективу берём содержанием, но не отдельным ритуалом:** она шаг той же
|
||||
сессии. Процесс личный, синхронизировать некого, а отдельная встреча ради трёх
|
||||
вопросов — та самая плата ритуалом без выгоды.
|
||||
|
||||
## Роли
|
||||
|
||||
**Человек** выбирает цель спринта, разбирает вопросы, держит право на
|
||||
необратимое и на истину в самих данных.
|
||||
|
||||
**Агент — оркестрация.** Он собирает набор под названную цель, ставит задачи,
|
||||
принимает отчёты и докладывает. Кто именно делает задачу — исполнитель, сабагент,
|
||||
пайплайн — дело проекта; сессия про это не знает и знать не должна.
|
||||
|
||||
## Единицы
|
||||
|
||||
- **Цель** — то, ради чего набирается спринт. Файл `[goal]`, перечисленный в
|
||||
`PLAN.md`. Цель постоянна: живёт, пока живёт направление.
|
||||
- **Задача** — то, что мерджится целиком и даёт видимую пользу.
|
||||
- **Вопрос** — решение человека. Не останавливает начатую работу, но **блокирует
|
||||
взятие** задачи в спринт. Живёт внутри файла задачи разделом «Вопросы» и тегом
|
||||
`question`.
|
||||
- **Блокер** — состояние, когда спринт не может продолжаться **ни одной**
|
||||
задачей.
|
||||
- **Спринт** — набор задач под одну цель, замороженный до его конца.
|
||||
|
||||
## Вопрос, блокер, необратимое
|
||||
|
||||
| | Что это | Когда спрашиваем | Что останавливает |
|
||||
| --- | --- | --- | --- |
|
||||
| **Вопрос** | решение человека | на сессии, пачкой | взятие задачи в спринт |
|
||||
| **Блокер** | спринт не может продолжаться ни одной задачей | немедленно | всё |
|
||||
|
||||
Право на **необратимое** — третье и отдельное: что именно необратимо, называет
|
||||
`CLAUDE.md` проекта, и спрашивается оно всегда, независимо от спринта.
|
||||
|
||||
**Блокер определяется исходом, а не одновременностью.** Встали разом или
|
||||
высыпались из спринта по одной — если продолжать нечем, это блокер: спринт
|
||||
распускается (`sprint close --dissolve --reason …`), человек спрашивается
|
||||
немедленно. Иначе спринт, из которого задачи вышли поштучно, выглядел бы штатно
|
||||
завершённым, а вопросы тихо ждали бы сессии.
|
||||
|
||||
**Отличать вопрос от застревания.** Правило про остаток принадлежит управлению
|
||||
задачами: оно решает, **сделана задача или вышла**, а это исход планирования, не
|
||||
исполнения. **Ниже канонический текст; пайплайн проекта на него ссылается, а не
|
||||
пересказывает** — два экземпляра одного правила разъезжаются, и разъезжаются
|
||||
незаметно, потому что расхождение видно только на редком входе.
|
||||
|
||||
> Есть остаток, который доводится без ответа, — задача продолжается, вопрос
|
||||
> записывается в файл. Остатка нет — задача выходит из спринта.
|
||||
|
||||
С двумя оговорками, без которых тест ошибается:
|
||||
|
||||
> **Остаток, который материализует нерешённое** — записывает в хранилище,
|
||||
> журнал, витрину **или наружу** состояние, зависящее от неотвеченного вопроса,
|
||||
> — **не остаток**. Решение поднимается до начала записи: откатить запись
|
||||
> дороже, чем подождать ответ, а иногда невозможно. «Наружу» — часть правила, а
|
||||
> не пример: выкладка, публикация и отправка данных третьей стороне не
|
||||
> откатываются тем более.
|
||||
|
||||
> **Пол для остатка:** остаток, из которого пропала польза, названная в хуке, —
|
||||
> это не сделанная задача, а вышедшая из спринта.
|
||||
|
||||
## Заморозка набора
|
||||
|
||||
**Цель одна.** Набор служит ей; задача, не служащая цели, в спринт не попадает,
|
||||
даже если взять удобно (`sprint take` это и запрещает). **Задача с открытым
|
||||
вопросом в набор не берётся.**
|
||||
|
||||
**Новая работа падает в беклог, а не в идущий спринт.** Решение «врываться или
|
||||
отложить» принимается один раз правилом, а не заново каждый раз. Врывается
|
||||
только два класса:
|
||||
|
||||
1. **Необратимый ущерб** — потеря, порча или утечка данных: то, что не чинится
|
||||
доделкой потом.
|
||||
2. **Сломан общий станок** — красная проверка, на которой стоит определение
|
||||
готовности **всех** задач набора. Это не новая работа, а починка того, на чём
|
||||
делается вся остальная.
|
||||
|
||||
Что в проекте считается необратимым ущербом и что — общим станком, называет
|
||||
`CLAUDE.md` проекта. Не названо — спрашиваем человека, а не решаем сами.
|
||||
|
||||
**Конец спринта** — когда каждая задача набора либо сделана, либо вышла с
|
||||
записанной причиной. Не «все сделаны»: иначе одна застрявшая задача держит
|
||||
спринт бесконечно. Пустой набор закрывается `sprint close` — скрипт не даст
|
||||
закрыть непустой.
|
||||
|
||||
Ведение спринта целиком — исходы задачи, определение готовности, приёмка,
|
||||
доклад — [references/sprint.md](references/sprint.md).
|
||||
|
||||
## Сессия: четыре шага в этом порядке
|
||||
|
||||
Это зависимость, а не список.
|
||||
|
||||
1. **Разбор вопросов.**
|
||||
2. **Разбор прошедшего спринта — про процесс, а не про задачи.**
|
||||
3. **Переоценка задач** порциями.
|
||||
4. **Выбор цели и набор спринта.** Цель называет человек, набор собирает агент и
|
||||
показывает **до старта работ**.
|
||||
|
||||
Процедура каждого шага, размер и отбор порции, храповик на залежавшихся, формат
|
||||
интерактива и доклад — [references/cadence.md](references/cadence.md).
|
||||
|
||||
## Инструмент
|
||||
|
||||
Тот же `tasks.py`, что у скилла `tasks` — оба скилла в одном плагине, путь
|
||||
общий: `tk="$CLAUDE_PLUGIN_ROOT/skills/tasks/scripts/tasks.py"`. Сессии нужны
|
||||
прежде всего:
|
||||
|
||||
```
|
||||
python3 $tk check --dir D # с этого начинается любая сессия
|
||||
python3 $tk list --dir D --questions # шаг 1: что накопилось
|
||||
python3 $tk list --dir D --tag sprint:<слаг> # шаг 3: урожай спринта, первая порция
|
||||
python3 $tk list --dir D --stale # шаг 3: дальше по залежалости
|
||||
python3 $tk list --dir D --goal <слаг> # шаг 4: кандидаты под названную цель
|
||||
python3 $tk sprint start --dir D --goal <слаг> # шаг 4: заводит и слаг спринта
|
||||
python3 $tk sprint take --dir D <слаг> … # шаг 4: набор
|
||||
python3 $tk sprint close --dir D # конец спринта; --dissolve при блокере
|
||||
python3 $tk reopen <слаг> --dir D --reason … # приёмка не сошлась после закрытия
|
||||
```
|
||||
|
||||
`D` — каталог задач проекта; цепочка его разрешения и вызов из чужого контекста
|
||||
описаны в скилле `tasks` («Переносимость»). **Коды выхода** — там же: 1 это
|
||||
дрейф в беклоге, 3 это «каталога нет», и ветвиться на них надо по-разному.
|
||||
|
||||
**Слаг спринта заводит `sprint start`** (по умолчанию — дата) и пишет его в
|
||||
`SPRINT.md`; всё заведённое при открытом спринте помечается `sprint:<слаг>`
|
||||
автоматически. Поэтому «первая порция — урожай прошедшего спринта» работает без
|
||||
чьей-либо памяти.
|
||||
|
||||
Правки задач делаются мутациями (`edit`, `move`, `close`), а не редактором:
|
||||
руками правится только тело файла. Это правило скилла `tasks`, здесь оно не
|
||||
пересказывается.
|
||||
|
||||
## Стимулы, которые процесс создаёт
|
||||
|
||||
Правило, которое можно обойти в свою пользу, будет обойдено.
|
||||
|
||||
**Приёмщик и исполнитель здесь совпадают, и это надо назвать вслух.** Задачу
|
||||
закрывает и двигает по индексам агент-оркестратор — тот же, кто её и сделал.
|
||||
Прежде границу держала механика: моста между плагинами не было, и закрыть задачу
|
||||
пайплайн физически не мог. Теперь мост есть, и защита у трёх обходов ниже —
|
||||
**только текстовая**. Опоры, которые остались настоящими:
|
||||
|
||||
- **отчёт триажа** в `openspec/changes/<id>/review/` — независимый артефакт,
|
||||
написанный ревью, а не исполнителем; по нему сверяют состав прогона и урожай;
|
||||
- **`SPRINT.md` под git** — `git log -p` показывает, что и когда было закрыто;
|
||||
- **`reopen <слаг> --reason`** — закрытие не окончательно. Приёмка человеком на
|
||||
сессии его отменяет, и это штатная операция, а не скандал.
|
||||
|
||||
Известные обходы:
|
||||
|
||||
- **Скрыть блокер** — он останавливает всё и выглядит как провал исполнителя.
|
||||
Защита: тест про остаток плюс прямая запись, что **объявление блокера
|
||||
неудачей не считается**.
|
||||
- **Не записать вопрос** на задаче-кандидате, чтобы не вычеркнуть её из
|
||||
ближайшего набора. Защита: вопросы кандидатов разбираются на той же сессии
|
||||
**вне очереди порции**.
|
||||
- **Занизить критерии приёмки**, раз они пол. Защита ослаблена: правит их тот же,
|
||||
кто по ним отчитывается. Остаётся требование, что расхождение критериев с
|
||||
сутью — **дефект критериев, о котором сообщают, а не молча дорабатывают**, и
|
||||
переоценка на сессии, где критерии видит человек.
|
||||
- **Сжать задачу до остатка** и отчитаться «сделана». Защита ослаблена там же.
|
||||
Пол для остатка — польза, названная в хуке; проверяет его человек при приёмке,
|
||||
и `reopen` — его инструмент.
|
||||
- **Занизить урожай** — не заводить найденное по ходу. Защита: поимённая сверка
|
||||
со **сохранённым отчётом триажа**, а не с прозой исполнителя. Каждая
|
||||
отложенная находка имеет либо слаг, либо строку «не заведена: причина».
|
||||
Нулевой урожай при непустом отчёте виден сразу.
|
||||
|
||||
Стимулы внутри пайплайна задачи (занизить требования к проверке, пропустить
|
||||
проход) принадлежат ему и защищены там же.
|
||||
|
||||
## Слоты проекта
|
||||
|
||||
Сессия не знает ни языка, ни сборки, ни CI. Часть проектного отвечает
|
||||
[канон](../canon/references/canon.md) структурой: разбор процесса (шаг 2) живёт
|
||||
в `docs/review.md`, оракулы и «чем краснеет безусловно» — в семантике гейта в
|
||||
`CLAUDE.md`. Остальное проект **дописывает в `CLAUDE.md`**:
|
||||
|
||||
1. **Пайплайн задачи** — чем задача выполняется и что входит в его определение
|
||||
готовности. Сессия требует только форму: пайплайн пройден + критерии приёмки
|
||||
проверены поимённо.
|
||||
2. **Общий станок** — какая проверка, покраснев, врывается в замороженный
|
||||
спринт.
|
||||
3. **Необратимое** — что спрашивается у человека всегда (тот же слот, что у
|
||||
скилла `tasks`; дом один).
|
||||
4. **Как критерии приёмки переживают удаление файла задачи** — файл удаляется
|
||||
при закрытии, поэтому критерии копируются туда, где их увидит приёмщик
|
||||
(предложение об изменении, описание ветки, тело коммита). Куда именно —
|
||||
решает проект.
|
||||
5. **Ориентир по размеру спринта**, если он замерялся. Умолчание — 5–8 задач, и
|
||||
это **ориентир, а не закон**.
|
||||
|
||||
Числа проекта (сколько задач в спринте, сколько времени на задачу, каков прирост
|
||||
беклога) — предмет шага 2, а не константы этого скилла.
|
||||
|
||||
## Чего этот скилл не делает
|
||||
|
||||
Не пишет код и не выполняет задачи. Не заводит и не переоформляет задачи сам по
|
||||
себе — формат и содержимое ведёт `tasks` (сессия зовёт его операции). Не решает
|
||||
за человека, какая цель следующая. Не двигает набор идущего спринта.
|
||||
@@ -0,0 +1,199 @@
|
||||
# Сессия: четыре шага
|
||||
|
||||
Одна сессия между спринтами. Порядок шагов — **зависимость, а не список**:
|
||||
переоценивать задачи, не разобрав вопросы, значит переоценивать вслепую; набирать
|
||||
спринт, не переоценив, значит набирать из протухшего.
|
||||
|
||||
Начинается сессия с `tasks.py check` (и `check --fix`, если дрейф накопился) —
|
||||
результат идёт строкой в доклад.
|
||||
|
||||
## Шаг 1. Разбор вопросов
|
||||
|
||||
`tasks.py list --questions` — всё, что накопилось. Вопрос это решение человека,
|
||||
и разбирается он **пачкой**, а не по одному в момент возникновения: по одному —
|
||||
это дёрганье, пачкой — это сессия.
|
||||
|
||||
Порядок по каждому вопросу:
|
||||
|
||||
1. **Проверь, не отвечен ли он уже** — решением, документом, соседним
|
||||
изменением, самим ходом прошедшего спринта. Отвеченный вопрос не выносится
|
||||
человеку: это самая частая находка и она не требует ничьего решения.
|
||||
2. **Сформулируй развилку** с вариантами и последствием каждого, рекомендация —
|
||||
первым вариантом.
|
||||
3. **Вынеси пачкой** через `AskUserQuestion`, не больше трёх за раз.
|
||||
4. **Ответ записывается в тело задачи**, тег снимается `edit <slug> --rm-tag
|
||||
question`, **хук переписывается**: «Решено: …» на вопрос «почему это лежит в
|
||||
беклоге» уже не отвечает.
|
||||
|
||||
**Вопросы на задачах-кандидатах разбираются вне очереди порции** — здесь же, на
|
||||
этой сессии, даже если сама задача в порцию переоценки не попала. Иначе правило
|
||||
«задача с открытым вопросом в набор не берётся» создаёт стимул вопрос не
|
||||
записывать, лишь бы не вычеркнуть задачу из ближайшего спринта.
|
||||
|
||||
## Шаг 2. Разбор прошедшего спринта — про процесс, а не про задачи
|
||||
|
||||
Не «что мы сделали» (это доклад спринта, он уже был), а:
|
||||
|
||||
- **что сломалось в процессе и почему не поймали** — промах, доехавший до конца;
|
||||
- **сколько на самом деле заняли задачи** против ожидания;
|
||||
- **какие правила не сработали или сработали не так** — в том числе правила
|
||||
этого плагина;
|
||||
- **какие числа пора пересмотреть** — ориентир по размеру спринта, прирост
|
||||
беклога на одну закрытую задачу, время на задачу. Эта обязанность иначе висит
|
||||
ничья: числа, помеченные как «первый замер», не пересматриваются никогда, если
|
||||
их не пересматривает конкретный шаг.
|
||||
|
||||
**Артефакт обязателен.** Вывод, оставшийся в контексте сессии, не существует:
|
||||
следующая сессия его не увидит. Дом у него один и известен из канона —
|
||||
**`docs/review.md`**: вывод про конвейер и про то, что перестали проверять, идёт
|
||||
в раздел настройки, вывод про воспроизведённый дефект — в журнал. Решение с
|
||||
долгим следом — в `docs/adr/`.
|
||||
|
||||
Отдельным ритуалом ретроспектива не выделяется: процесс личный,
|
||||
синхронизировать некого.
|
||||
|
||||
## Шаг 3. Переоценка задач
|
||||
|
||||
Цель — выкинуть то, что перестало быть задачей, и вернуть остальному честное
|
||||
состояние. Не «пересмотреть всё», а «пересмотреть порцию до конца».
|
||||
|
||||
### Порция и правило остановки
|
||||
|
||||
Тридцать задач за один заход — это усталость и штамповка: последние десять
|
||||
получат «оставить» не потому, что живы, а потому, что сессия затянулась.
|
||||
|
||||
- **5–8 задач за порцию.** Размер обоснован усталостью, а не пропускной
|
||||
способностью, и менять его не надо — **надо брать несколько порций за
|
||||
сессию**.
|
||||
- **Сколько порций:** не меньше `⌈урожай прошедшего спринта / 8⌉`. Урожай — это
|
||||
задачи, заведённые за спринт; при урожае в 15 это две-три порции.
|
||||
- **Отбор порций по порядку:**
|
||||
1. **урожай спринта** — `list --tag sprint:<слаг>`: свежезаведённое ещё не
|
||||
проходило ни одной проверки на нужность. Слаг спринта берётся из
|
||||
`SPRINT.md` (его завёл `sprint start`), тег на задачах проставлен
|
||||
автоматически при заведении — руками не метят и не вспоминают;
|
||||
2. дальше **по залежалости** — `list --stale`;
|
||||
3. по потребности — одна секция целиком, один тег (партия ревью), одна цель
|
||||
(`--goal`), список от пользователя.
|
||||
- **Останавливайся на границе порции**, даже если «ещё чуть-чуть осталось».
|
||||
Между порциями — промежуточный доклад.
|
||||
|
||||
### Что делать с каждой задачей
|
||||
|
||||
Сперва то, что не требует ничьего решения:
|
||||
|
||||
1. **Проверь, не сделано ли уже.** Задача, реализованная попутно в соседнем
|
||||
изменении, — самая частая находка. Смотри код, документацию, историю коммитов
|
||||
по ключевым словам. Удаление «как реализованной» деструктивно и без следа
|
||||
(в `REJECTED.md` реализованные не пишутся), поэтому порог улики жёсткий:
|
||||
`close <slug> --implemented` только имея **конкретный коммит или строку
|
||||
документа**, закрывающие задачу, и ссылка идёт в доклад. Есть лишь косвенные
|
||||
признаки — не удаляй сам, вынеси в пачку вопросов. Сделана частично → задача
|
||||
сжимается до остатка: тело правишь редактором, заголовок и хук — через
|
||||
`edit`.
|
||||
2. **Проверь, не отменена ли решением.** Документ, ADR или архивное изменение
|
||||
мог закрыть вопрос иначе — тогда `close <slug> --reason "<ссылка на
|
||||
решение>"`. Задача закрывается не только коммитом.
|
||||
3. **Проверь пересечения.** Две задачи об одном — содержимое в одну, вторую
|
||||
`close <slug> --reason "слита с <другой-слаг>"`. Смотри **шире порции**:
|
||||
интейк дедуплицирует новое против существующего, но никогда не
|
||||
пересматривает уже лежащее, и две задачи с одной причиной могут лежать рядом
|
||||
месяцами.
|
||||
4. **Пере-кластеризуй по общей причине.** Несколько задач, оказавшихся симптомами
|
||||
одного дефекта, сливаются в одну — это находка, которую интейк дать не мог.
|
||||
5. **Гигиена полей** — протухший хук, вопрос в прозе, снятый ответ, свойство
|
||||
репозитория в рамках, предписание процесса в теле. Список и правила — в
|
||||
скилле `tasks`.
|
||||
|
||||
Затем — то, что решает пользователь:
|
||||
|
||||
6. **Жива ли она вообще.** Контекст мог измениться: ушла зависимость, отпал
|
||||
сценарий, обошли иначе. Здесь и звучит вопрос о выкидывании.
|
||||
7. **Та ли цель.** Приоритетов нет, и «повысить» нечего — вместо повышения
|
||||
**смена цели** (`edit <slug> --goal <другой>`) или включение в ближайший
|
||||
набор. Задача, которой не находится цель, — кандидат на выход: она не попадёт
|
||||
ни в один спринт.
|
||||
8. **Задача ли это по-прежнему.** Не проходит тест «готова к взятию» → `edit
|
||||
<slug> --type idea`, дальше штурм. Разрослась → `edit <slug> --type epic`,
|
||||
дальше декомпозиция.
|
||||
9. **Переоценка по измеренному.** Спринт производит числа — сколько на самом
|
||||
деле стоит такая работа, что оказалось дороже ожидания. Эти числа меняют цену
|
||||
**других** задач, и именно здесь это применяется: задача, чья цена выросла
|
||||
втрое, а польза осталась прежней, — кандидат на выход.
|
||||
|
||||
### Храповик на залежавшихся
|
||||
|
||||
Сильно залежавшаяся задача — сигнал сама по себе: её либо ни разу не собирались
|
||||
делать, либо нечем взять. Измеряй наблюдаемым — датой последней правки из git
|
||||
(`list --stale` ставит такие первыми); счётчик «сколько сессий пережила» нигде
|
||||
не хранится.
|
||||
|
||||
Задача из верхних строк `--stale`, которую и этот заход оставляет без изменений,
|
||||
**либо двигается (меняет цель, идёт в набор, уходит с причиной), либо остаётся с
|
||||
явно записанной причиной**, почему её держим (`move <slug> --section <та же>
|
||||
--reason …`). Молчаливое «оставить как есть» на давно неподвижной задаче — это
|
||||
решение не принимать решение; запись причины превращает его в осознанное и не
|
||||
даёт тому же вопросу всплыть на следующей сессии.
|
||||
|
||||
### Интерактив
|
||||
|
||||
- Вопросы — через `AskUserQuestion`, **не больше трёх за раз**. Порция в 5–8
|
||||
задач обычно даёт больше трёх суждений — тогда веди несколько итераций по ≤3,
|
||||
а не по одному на задачу и не одним перегруженным запросом.
|
||||
- К каждому варианту — **предварительное суждение, рекомендация первым
|
||||
вариантом**: «предлагаю выкинуть, потому что …». Пользователю дешевле
|
||||
возразить, чем судить с нуля.
|
||||
- Всё, что решается фактом (сделано / отменено / дублируется), решай сам и
|
||||
показывай списком в докладе, а не выноси в вопросы.
|
||||
|
||||
Пример одной итерации — три залежавшихся задачи, механику по ним уже разобрали:
|
||||
|
||||
> **Переоценка: 3 залежавшихся (порция по `--stale`)**
|
||||
>
|
||||
> 1. `versii-kachestvo-repaki` — версии и качество одного тайтла
|
||||
> - Выкинуть *(рекомендую)* — помечена «не боль», за полгода ни разу не возникла
|
||||
> - Оставить под целью `nadyozhnost-razdach`
|
||||
> - Перевести под цель `kachestvo-mediateki` — там она первая в очереди
|
||||
> 2. `backup-sqlite` — бэкап базы
|
||||
> - Оставить под текущей целью *(рекомендую)* — не сработала, но риск реальный
|
||||
> - Взять в ближайший набор — без бэкапа ретеншн опасен
|
||||
> - Выкинуть
|
||||
> 3. `guessit-sputnik` — вынести распознавание в сервис-спутник
|
||||
> - Понизить до `[idea]` *(рекомендую)* — не проходит тест «готова к взятию»
|
||||
> - Оставить задачей
|
||||
|
||||
Каждый вариант несёт причину — ту самую, что уедет в `--reason`. Ответы применяй
|
||||
сразу и, если в порции осталось ещё, следующей итерацией показывай следующие ≤3.
|
||||
|
||||
## Шаг 4. Выбор цели и набор спринта
|
||||
|
||||
1. **Покажи состояние целей**: линия `PLAN.md` с обоснованием порядка, кусты, и
|
||||
по каждой цели-кандидату — сколько под ней задач без открытых вопросов
|
||||
(`list --goal <слаг>`). Цель без готовых задач набором не станет: её сперва
|
||||
надо декомпозировать.
|
||||
2. **Цель называет человек.** Это продуктовое решение, а не механика: агент
|
||||
предлагает и объясняет, но не выбирает.
|
||||
3. **Набор собирает агент** — `sprint start --goal <слаг>`, затем `sprint take
|
||||
…`. Скрипт не даст взять чужую цель, идею, эпик, задачу с открытым вопросом
|
||||
или без критериев приёмки.
|
||||
4. **Набор показывается человеку до старта работ.** Показ — это и есть момент
|
||||
заморозки: после него набор не двигается.
|
||||
5. Задача, которой для взятия не хватает только критериев приёмки, дописывается
|
||||
здесь же — 2–5 утверждений, у каждого назван оракул (меньше двух `sprint
|
||||
take` не примет). Но если для критериев нужен ответ человека, это вопрос, и
|
||||
задача в набор не идёт.
|
||||
|
||||
**Размер — ориентир, а не закон:** 5–8 задач. Можно взять больше, можно меньше —
|
||||
набор под цель важнее круглого числа; одна крупная задача спринтом тоже бывает.
|
||||
|
||||
## Доклад сессии
|
||||
|
||||
- Что просмотрено: N из M, сколько порций, по какому признаку отобраны.
|
||||
- Вопросы: разобрано N, из них отвечено без человека N, снято тегов N.
|
||||
- Разбор процесса: что записано и куда.
|
||||
- Изменения списком: удалено как реализованное (со ссылками), ушло без
|
||||
реализации (с причинами), понижено до идей, слито, сменило цель.
|
||||
- Новый спринт: цель, набор со слагами, дата.
|
||||
- **Границы покрытия**: сколько задач не трогали и какие именно секции, теги или
|
||||
цели остались — иначе доклад читается как «беклог разобран».
|
||||
- `tasks.py check` после правок — результат строкой.
|
||||
@@ -0,0 +1,123 @@
|
||||
# Ведение спринта
|
||||
|
||||
Спринт — набор задач под одну цель, замороженный до его конца. Здесь то, что
|
||||
происходит **внутри** спринта: как задача заканчивается, что считается сделанным,
|
||||
кто принимает и что идёт в доклад. Как спринт набирается — шаг 4 в
|
||||
[cadence.md](cadence.md).
|
||||
|
||||
## Наблюдаемые исходы задачи
|
||||
|
||||
Как они достигаются — дело пайплайна проекта. Сессия знает только исход и его
|
||||
след.
|
||||
|
||||
- **Сделана** — по определению готовности ниже. `close <slug> --implemented`:
|
||||
файл и строка удаляются, следом остаётся коммит. **Закрывает владелец спринта
|
||||
и только после вердикта приёмки** — см. «Кто и когда закрывает».
|
||||
- **Вышла из спринта** — `sprint drop <slug> --reason …`: возвращается в беклог
|
||||
с вопросом в файле и **без живого незакоммиченного предложения** — иначе при
|
||||
следующем взятии оно столкнётся с новым. Наработки, которые жалко терять,
|
||||
переезжают в тело задачи текстом.
|
||||
- **Переросла в эпик** — распознаётся **до того, как под неё заведено
|
||||
предложение об изменении**, иначе его придётся выбрасывать. Помечается
|
||||
`[epic]`, выходит из набора, уходит на декомпозицию; спринт продолжается
|
||||
остальными, части в замороженный набор не добавляются.
|
||||
- **Отменена решением по ходу** — `close <slug> --reason "<ссылка на решение>"`
|
||||
прямо из спринта. Это редкий, но законный исход, и он называется в докладе.
|
||||
|
||||
**Конец спринта** — когда по каждой задаче набора наступил один из исходов. Не
|
||||
«все сделаны»: иначе одна застрявшая задача держит спринт бесконечно. Затем
|
||||
`sprint close`.
|
||||
|
||||
**Урожай заводится при закрытии спринта, а не при закрытии задачи.** Это
|
||||
обязанность закрывающего: пройти по спискам находок от исполнителей и завести
|
||||
недостающее интейком скилла `tasks` — с дедупликацией и картой человеку. Заводимое
|
||||
метится тегом спринта само (`sprint:<слаг>`), поэтому первая порция следующей
|
||||
сессии поднимается одной командой `list --tag sprint:<слаг>`. Спринт, закрытый
|
||||
без этого шага, оставляет находки жить в отчётах — то есть нигде.
|
||||
|
||||
**Провал спринта.** Сработал блокер — спринт распускается (`sprint close
|
||||
--dissolve --reason …`), недоделанное возвращается в беклог, новый набор
|
||||
делается после ответа человека. Спринт не «ждёт»: ждать может человек, а
|
||||
замороженный набор, который нельзя двигать, только мешает.
|
||||
|
||||
## Определение готовности
|
||||
|
||||
Задача засчитывается сделанной, когда верно **всё**:
|
||||
|
||||
1. **Пайплайн задачи пройден до конца** — со своим определением готовности, за
|
||||
которое отвечает проект: проверки, состав ревью, документация, коммит. Здесь
|
||||
оно не пересказывается и не подменяется — **форма фиксирована, содержание
|
||||
даёт `CLAUDE.md` проекта**. Пайплайна нет, задача сделана руками — условие
|
||||
читается как «проверки проекта зелёные и изменение влито».
|
||||
2. **Критерии приёмки проверены поимённо** — каждый со своим оракулом, исход по
|
||||
каждому назван. Это единственное, что добавляет управление задачами: пайплайн
|
||||
отвечает «сделано по правилам», критерии — «сделано то, что заказывали».
|
||||
3. **Находки по ходу отданы списком** — исполнитель обязан их **назвать**
|
||||
(каждую, с пометкой «заведена / не заведена: причина»), но **не обязан
|
||||
заводить**: заведение интерактивно, оно требует дедупликации против беклога и
|
||||
кладбища и решений человека. Обязанность **завести урожай** — на закрытии
|
||||
спринта, ниже. Так автономный исполнитель не оказывается одновременно обязан
|
||||
завести задачи и не вправе это сделать в одиночку.
|
||||
|
||||
### Кто и когда закрывает
|
||||
|
||||
**Задачу закрывает не пайплайн, а владелец спринта — после приёмки.** Порядок:
|
||||
|
||||
1. пайплайн доводит задачу до коммита и **докладывает исход**; файл задачи он не
|
||||
трогает — своей процедуры закрытия у него нет;
|
||||
2. приёмщик (не исполнитель) сверяет критерии поимённо и выносит вердикт;
|
||||
3. вердикт сошёлся — владелец спринта зовёт `close <slug> --implemented`
|
||||
командой учёта задач из `CLAUDE.md` проекта.
|
||||
|
||||
Обратный порядок ломает приёмку физически: закрытие **удаляет файл**, и
|
||||
приёмщику, нашедшему расхождение, возвращать нечего.
|
||||
|
||||
**Дорога назад существует и обязана быть названа.** Закрыли раньше вердикта, а
|
||||
приёмка не сошлась — `tasks.py reopen <slug> --reason "приёмка не сошлась: …"`:
|
||||
файл восстанавливается из истории git, строка возвращается в набор идущего
|
||||
спринта (или в беклог, если спринта нет), строка кладбища снимается. Тело
|
||||
восстанавливается **на момент удаления** — всё, что было дописано позже, живёт
|
||||
только в коммите задачи, и это называется в докладе.
|
||||
|
||||
### Кто и по чему принимает
|
||||
|
||||
Три условия, без которых пункт про критерии не исполняется никем:
|
||||
|
||||
1. **Критерии переживают файл задачи.** Файл удаляется при закрытии, поэтому
|
||||
критерии копируются туда, где их увидит приёмщик — в предложение об
|
||||
изменении, описание ветки, тело коммита. Куда именно, называет `CLAUDE.md`
|
||||
проекта. Иначе приёмка проверяет критерии из файла, которого больше нет.
|
||||
2. **Принимает не исполнитель.** Отдельный контекст — сабагент или человек, —
|
||||
которому дают изменение, отчёт исполнителя и отчёты ревью. Тот же принцип
|
||||
декорреляции, на котором стоит любой конвейер проверки: занизивший и
|
||||
проверяющий не должны быть одним контекстом.
|
||||
3. **Расхождение — дефект критериев.** Приёмщик правит критерии и возвращает
|
||||
задачу исполнителю **в этом же спринте**: ответ есть, остаток есть, по тесту
|
||||
про остаток это не выход из спринта.
|
||||
|
||||
## Что врывается в замороженный набор
|
||||
|
||||
Только два класса — правило и его обоснование в SKILL.md. Здесь механика:
|
||||
|
||||
- вторжение **не добавляет** задачу в набор: `SPRINT.md` остаётся набором под
|
||||
цель. Внеплановая работа делается и называется в докладе отдельной строкой
|
||||
«внеплановое: что и почему»;
|
||||
- если внеплановое требует больше пары часов, честнее распустить спринт, чем
|
||||
делать вид, что набор соблюдается;
|
||||
- всё остальное падает в беклог через обычный интейк и ждёт сессии.
|
||||
|
||||
## Доклад в конце спринта
|
||||
|
||||
Проверяемые якоря, а не пересказ:
|
||||
|
||||
- **Цель спринта** и по каждой задаче набора: **хеш коммита**, дословный исход
|
||||
проверок проекта, **исход по каждому критерию приёмки**.
|
||||
- **Какие развилки решались** и чем обоснованы.
|
||||
- **Урожай:** сколько задач заведено, какие вопросы накопились, что вышло из
|
||||
спринта и почему, что было внеплановым.
|
||||
- **Поимённая сверка урожая** с отчётами ревью: каждая отложенная находка имеет
|
||||
либо слаг, либо строку «не заведена: причина». Нулевой урожай при непустом
|
||||
отчёте — сигнал, а не благополучие.
|
||||
- **Созрела ли порция для сессии.** Решение звать — человека, напоминание —
|
||||
обязанность агента: `⌈урожай / 8⌉` порций.
|
||||
- **Границы покрытия** сжатой строкой: что в этом спринте не проверялось вовсе.
|
||||
@@ -0,0 +1,331 @@
|
||||
---
|
||||
name: tasks
|
||||
description: Ведение задач и целей как каталога markdown-файлов (одна запись = один файл в items/ + строка в одном из индексов). Заведение задачи, идеи или цели из диалога, разбор находок аудита/ревью, декомпозиция на независимо полезные части, мозговой штурм идеи, гигиена полей и проверка согласованности индексов. Использовать, когда просят добавить задачу/идею/цель, превратить находки ревью в задачи, разбить задачу, проработать идею, поправить формат или проверить беклог. Ритуал между спринтами — скилл session. Не реализует задачи — этим занимается пайплайн проекта.
|
||||
---
|
||||
|
||||
# Задачи
|
||||
|
||||
Задачи — каталог markdown-файлов. Одна запись = один файл `items/<slug>.md` плюс
|
||||
строка **ровно в одном** индексе. Скилл владеет **форматом и содержимым**:
|
||||
заводит, редактирует, закрывает, разбирает находки ревью, дробит, штурмует идеи.
|
||||
|
||||
Чем он **не** владеет: ритуалом между спринтами (разбор вопросов → разбор
|
||||
прошедшего спринта → переоценка → выбор цели и набор) — это скилл `session`; и
|
||||
выполнением задачи — это пайплайн проекта.
|
||||
|
||||
## Четыре правила, из которых всё следует
|
||||
|
||||
Ситуация не покрыта инструкцией — решай по ним.
|
||||
|
||||
1. **Беклог гниёт с той стороны, где его пополняют.** Заведение — самая частая
|
||||
операция и с худшим отказом: из одного разговора рождается пять файлов, а
|
||||
переоценка потом разгребает то, чего не надо было заводить. Дедупликация и
|
||||
фильтр на входе дешевле любой чистки. Заводим только то, что **не делаем
|
||||
сейчас** и о потере чего пожалеем.
|
||||
2. **Файл — источник истины, индексы производны.** Разошлись — неправы индексы.
|
||||
Согласованность механизируема и проверяется командой, а не вниманием: всё,
|
||||
что ловит `tasks.py check`, не должно попадать ни в чек-лист, ни в промпт.
|
||||
Поэтому **хук живёт в мета-строке файла**, а строка индекса его лишь
|
||||
повторяет: пока хук лежал только в индексе, восстановление пропавшей строки
|
||||
теряло его молча и навсегда. Единственное исключение намеренное: **в каком
|
||||
индексе лежит задача, знают индексы** — «в спринте» это свойство спринта, а
|
||||
не файла, поля-состояния нет.
|
||||
3. **Причина переживает запись.** Выкинутая без причины задача вернётся через
|
||||
квартал тем же текстом. Реализованная оставляет след в коммите — выкинутая не
|
||||
оставляет ничего, поэтому у неё есть `REJECTED.md`.
|
||||
4. **Порядка нет, есть цель.** Ни в секциях, ни списком: «что делать дальше»
|
||||
отвечает набор спринта, а между спринтами порядок не нужен никому — брать
|
||||
задачи вне спринта запрещает заморозка. Поэтому нет ни приоритетов, ни
|
||||
«повысить», ни «встать раньше»: вместо повышения — смена цели или включение
|
||||
в набор.
|
||||
|
||||
## Раскладка
|
||||
|
||||
Каталог задач — **`docs/tasks`, жёстко**: это часть
|
||||
[канона документов](../canon/references/canon.md), и подгоняется под него
|
||||
проект, а не наоборот.
|
||||
|
||||
```
|
||||
docs/tasks/
|
||||
items/ задачи и цели файлами, <slug>.md, слаги английские
|
||||
PLAN.md оглавление целей: линия (упорядоченная) и кусты
|
||||
BACKLOG.md что можно взять — только задачи, целей здесь нет
|
||||
SPRINT.md текущий спринт: цель, набор, дата
|
||||
REJECTED.md ушедшее БЕЗ реализации, с причиной и датой
|
||||
```
|
||||
|
||||
Правило, снимающее путаницу: **`BACKLOG.md` — то, что берут; `PLAN.md` — то,
|
||||
подо что берут.** Цель в спринт взять нельзя, поэтому в списке берущихся ей не
|
||||
место.
|
||||
|
||||
**Секции «блокеры» в беклоге нет.** Блокер — это *состояние* (спринт не может
|
||||
продолжаться ни одной задачей), а не полка: он живёт ровно до ответа человека, и
|
||||
записи в такой секции не успевают жить. Следы блокера остаются вопросами в
|
||||
файлах задач распущенного спринта. Постоянно пустая секция со старой семантикой
|
||||
«разбираются пачками» противоречила бы правилу «блокер эскалируется немедленно»,
|
||||
поэтому `init` её заводить отказывается, а `check` о ней говорит. **Проекту,
|
||||
который переезжает с такой секцией, её надо удалить** — это единственное место,
|
||||
где это сказано.
|
||||
|
||||
**Задача живёт в одном индексе за раз.** Взята в спринт — строка переезжает из
|
||||
`BACKLOG.md` в `SPRINT.md`; вышла — обратно. Файл в `items/` при этом **не
|
||||
двигается**: он и есть запись, индексы лишь показывают, где она числится.
|
||||
|
||||
**У сделанной задачи записи не остаётся** — файл и строка удаляются (`close
|
||||
--implemented`). Ей хватает коммита и документации проекта; вторая запись была
|
||||
бы вторым домом для того же факта. Вопрос «что было в спринте N» отвечается
|
||||
даром: `SPRINT.md` лежит под git, `git log -p docs/tasks/SPRINT.md` отдаёт историю
|
||||
всех наборов без отдельного журнала.
|
||||
|
||||
## Цели
|
||||
|
||||
**Цель — такой же файл в `items/`, тип `[goal]`**, перечисленный в `PLAN.md`:
|
||||
либо звено упорядоченной **линии** продукта (с обоснованием порядка прозой),
|
||||
либо тематический **куст** — цель, в последовательность не встающая («прочность
|
||||
слияния», «журнал и пересборка»). Без второй части половина целей была бы нигде
|
||||
не перечислена: находки ревью не служат ничему из линии.
|
||||
|
||||
- **Список задач цели выводится, а не хранится.** В теле цели — зачем она и что
|
||||
считается её завершением; перечня задач там нет. Он был бы третьим индексом и
|
||||
поехал бы на первой же закрытой задаче, а `check` про него не знает. Связь
|
||||
однонаправленна: задача несёт тег `goal:<слаг>`, перечень даёт
|
||||
`tasks.py list --goal <слаг>`.
|
||||
- **Статус цели выводится.** Цель закрыта, когда у неё не осталось открытых
|
||||
задач; `[x]`/`[~]` руками не ведутся, а `close` цели с живыми задачами
|
||||
скрипт запретит. Единственная оговорка: цель без задач неотличима — «ещё не
|
||||
разобрана» или «всё закрыто». Различает **тег `decomposed`** в мета-строке
|
||||
цели: он ставится, когда цель разложена на задачи. Тег, а не строка в теле —
|
||||
потому что проверяется механически: `check` требует его у пустой цели, а
|
||||
`check --fix` сам проставляет его цели, у которой задачи есть.
|
||||
- **`[goal]` и `[epic]` — разные вещи.** Цель **постоянна**: живёт, пока живёт
|
||||
направление. Эпик **временен**: это задача, которая не мерджится целиком, её
|
||||
разбирают, и он исчезает. Два срока жизни под одним словом разъезжаются,
|
||||
поэтому слова два.
|
||||
|
||||
## Инструмент (`tasks.py`)
|
||||
|
||||
Пусть `tk="$CLAUDE_PLUGIN_ROOT/skills/tasks/scripts/tasks.py"`, а `D` —
|
||||
`docs/tasks` от корня проекта. `--dir` стоит в примерах намеренно: вызов из
|
||||
подкаталога — обычное дело.
|
||||
|
||||
```
|
||||
python3 $tk check --dir D # согласованность индексов + здоровье
|
||||
python3 $tk check --dir D --fix # + починить дрейф (секция, заголовок, дубли, хук, дом)
|
||||
python3 $tk list --dir D [--stale] [--section S] [--type T] [--tag a,b] [--goal S] [--index …] [--questions]
|
||||
python3 $tk add --dir D --slug S --title T [--type goal|idea|epic] [--section S] [--goal G] [--hook H] [--tag a,b]
|
||||
python3 $tk edit S --dir D [--title T] [--hook H] [--type T] [--goal G] [--add-tag a,b] [--rm-tag c]
|
||||
python3 $tk move S --dir D --section S [--reason R] [--after S | --first]
|
||||
python3 $tk close S --dir D --reason R # в REJECTED.md + удалить (ушла без реализации)
|
||||
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 … # разовая адаптация, references/adopt.md
|
||||
```
|
||||
|
||||
**Коды выхода — единый словарь; на нём ветвятся скиллы, а не на тексте вывода:**
|
||||
|
||||
| Код | Что случилось | Что делать |
|
||||
| --- | --- | --- |
|
||||
| 0 | сошлось / сделано | дальше по сценарию |
|
||||
| 1 | **только `check`:** найден дрейф индексов и файлов | `check --fix`, остаток разобрать |
|
||||
| 2 | ошибка употребления: аргументы или нарушенное правило | читать сообщение, это отказ по существу |
|
||||
| 3 | окружение: каталог не найден, конфиг битый или мимо диска | чинится путём или `docs/.pm.json`, повтор не поможет |
|
||||
| 4 | внутренний сбой | дефект скрипта, доложить |
|
||||
|
||||
Различать 1 и 3 обязательно: «дрейф в беклоге» — рабочая ситуация, «каталога
|
||||
нет» — нерабочая, и одинаковая реакция на них была бы неверна в обоих случаях.
|
||||
|
||||
Тип — английское ключевое слово `goal` / `idea` / `epic` / `task` (как и прочие
|
||||
токены команд); `task` префикса не несёт, остальные кодируются `[goal]`/
|
||||
`[idea]`/`[epic]` в заголовке. Текст задачи при этом русский.
|
||||
|
||||
**Мутации правят файл и индексы заодно** — руками строку индекса или мета-строку
|
||||
не пиши, зови `add`/`edit`/`move`/`close`/`sprint`. Смена заголовка, хука, типа,
|
||||
цели и **тегов** — это `edit`: он держит H1, мета-строку и индекс в синхроне.
|
||||
Снятие тега — `--rm-tag` (после ответа на вопрос снимается `question`), смена
|
||||
цели — `--goal`, он заменяет прежний `goal:*`.
|
||||
|
||||
**Переезд между индексами — следствие смены типа, а не отдельная команда.**
|
||||
`edit <slug> --type goal --section <часть плана>` переносит строку из
|
||||
`BACKLOG.md` в `PLAN.md` (и обратно `--type task --section <секция беклога>`);
|
||||
`move` двигает только внутри одного индекса и пишет причину. `--section` у
|
||||
`edit` работает **только** при таком переезде — иначе он отсылает к `move`,
|
||||
потому что смена секции без причины и есть тот дрейф, который потом никто не
|
||||
объяснит. Задача в наборе спринта тип не меняет вовсе: сперва `sprint drop`.
|
||||
|
||||
Тело задачи скрипт не трогает:
|
||||
`add` кладёт заголовок, мета-строку и шаблон с подсказками, тело дописываешь
|
||||
редактором (пока плейсхолдер на месте, `check` напоминает).
|
||||
|
||||
`check` — единственный судья согласованности; что именно он ловит, скажет его
|
||||
вывод, здесь не пересказываем. Гоняй его **в начале сессии** и **после каждой
|
||||
правки**, даже если правил мутациями: дрейф мог накопиться раньше. Накопившееся
|
||||
чини `check --fix` — он детерминированно правит то, где истина однозначна
|
||||
(секция, заголовок, дубли, хук из индекса в файл, строка в чужом индексе,
|
||||
пометка `decomposed` у цели с задачами), а неоднозначное (ссылка на исчезнувший
|
||||
файл, задача сразу в двух индексах) печатает отдельной пометкой
|
||||
`НЕОДНОЗНАЧНО` — это тебе, и это идёт строкой доклада.
|
||||
|
||||
`--fix` правит **и файлы** — ровно в двух местах, где источник ровно один и
|
||||
выбирать не из чего: хук, оставшийся только в индексе, переезжает в мета-строку,
|
||||
и цель, у которой есть задачи, получает тег `decomposed`. Оба случая печатаются
|
||||
поимённо.
|
||||
|
||||
**Что механизировано, а что нет.** Критерии приёмки проверяются у задачи, взятой
|
||||
в набор (`sprint take` и `check` по задачам спринта): число пунктов — жёстко
|
||||
(меньше двух — отказ, больше пяти — замечание), наличие оракула — **эвристикой**
|
||||
по слову «оракул» в пункте. Настоящий оракул от слова «оракул» машина не
|
||||
отличает, поэтому эвристика даёт только замечание, и в докладе это называется
|
||||
как есть: «проверено число пунктов, годность оракулов — глазами».
|
||||
|
||||
Формат файла, мета-строки, слага, индексов и `REJECTED.md` —
|
||||
[references/task-format.md](references/task-format.md). Там же тест «готова к
|
||||
взятию» и требования к критериям приёмки.
|
||||
|
||||
## Сценарии
|
||||
|
||||
### Завести задачу, идею или цель из диалога
|
||||
|
||||
1. **Фильтр.** Делаем прямо сейчас — не заводим. Не пожалеем о потере — не
|
||||
заводим. Родилось три кандидата — покажи их и спроси, какие заводить: молча
|
||||
заведённая пачка и есть тот самый отказ из правила 1.
|
||||
2. **Дедуп.** `list` плюс поиск по слагам, хукам и телам (`grep -ril`),
|
||||
**включая `REJECTED.md`**. Нашлось среди живых — **дописываем в существующий
|
||||
файл**, а не заводим соседний. Нашлось в `REJECTED.md` — покажи пользователю
|
||||
ту строку и что изменилось с момента отказа (`add` предупредит и сам, но
|
||||
молча заводить нельзя). Две задачи об одном — самая дорогая находка
|
||||
переоценки.
|
||||
3. **Тип по тесту готовности** (см. task-format): проходит — задача, не
|
||||
проходит — идея (`--type idea`), проходит по пользе, но не делается одним
|
||||
заходом — эпик (`--type epic`, сперва декомпозиция). Направление, а не
|
||||
работа — цель (`--type goal`).
|
||||
4. **Цель задачи.** У каждой задачи должен быть `--goal <слаг>`: задача вне цели
|
||||
не попадёт ни в один спринт. Подходящей цели нет — либо она заводится
|
||||
(`--type goal` кустом), либо это сигнал, что задача никому не служит и
|
||||
заводить её не надо. У идеи цели может не быть — она проставляется, когда
|
||||
идея становится задачей.
|
||||
5. `add …`, затем допиши тело редактором: одна фраза, критерии приёмки с
|
||||
оракулами, рамки. Хук отвечает «почему это лежит в беклоге» — состояние,
|
||||
остаток, боль, — а не пересказывает первый абзац, и пишется **для человека**:
|
||||
не «канонизация внутри транзакции», а «тело 40 МиБ держит блокировку 5 секунд,
|
||||
соседние доставки уходят в отказ».
|
||||
6. `check`.
|
||||
|
||||
### Разобрать находки аудита или ревью
|
||||
|
||||
Ревью и аудиты — тоже источник задач, но с зеркальной диалогу опасностью: не
|
||||
пять файлов из одной мысли, а сорок файлов из сорока сырых находок. Защита та
|
||||
же, что в самом ревью: кластеризация по причине, дедуп против живых и
|
||||
`REJECTED.md`, находка без свидетельства → идея, а не задача, и карта кластеров
|
||||
пользователю до создания файлов. Порядок, отображение серьёзности и привязка к
|
||||
целям — [references/from-review.md](references/from-review.md).
|
||||
|
||||
### Прийти в репозиторий, где задачи уже как-то ведутся
|
||||
|
||||
Разовая операция: вывести каталог задач из старой раскладки беклога, `TODO.md`,
|
||||
заметок или списка шагов в плане — [references/adopt.md](references/adopt.md).
|
||||
Сюда же относится переименование транслитных слагов в английские: оно делается
|
||||
**одним проходом вместе с починкой перекрёстных ссылок**, а не по одному слагу.
|
||||
|
||||
Если переводить надо не только задачи, а весь `docs/` — это скилл
|
||||
`av-dev-pm:canon`, и он зовёт этот сценарий сам на своём шаге.
|
||||
|
||||
### Декомпозиция и штурм идеи
|
||||
|
||||
[references/split.md](references/split.md). Обе операции превращают одну запись в
|
||||
несколько, и у обеих есть проверяемый тест: части должны **мерджиться порознь** и
|
||||
**каждая давать видимую пользу**, а у штурма исход «выкинуть» — полноправный.
|
||||
|
||||
### Гигиена полей
|
||||
|
||||
Правится по ходу любой операции, которая задачи касается (но не «заодно» по
|
||||
всему беклогу):
|
||||
|
||||
- **протухший хук** — задача изменилась, а хук отвечает на старый вопрос;
|
||||
особенно после ответа на вопрос задачи: «Решено: …» на «почему это лежит в
|
||||
беклоге» уже не отвечает. Переписывается `edit <slug> --hook …` — он правит
|
||||
мета-строку файла и строку индекса заодно;
|
||||
- **вопрос, застрявший в прозе** — вынимается в раздел «Вопросы» плюс тег
|
||||
`question` (`edit --add-tag question`), иначе он не виден ни `list
|
||||
--questions`, ни правилу «задача с открытым вопросом в набор не берётся»;
|
||||
- **тег, который некому снять** — `question` после ответа снимается `edit
|
||||
--rm-tag question` вместе с записью ответа в тело;
|
||||
- **свойство репозитория в рамках** — номер миграции, хеш, версия зависимости:
|
||||
в лежалой задаче протухает молча и становится ложной рамкой. Снимается;
|
||||
снимок берётся при постановке, а не при заведении;
|
||||
- **предписание процесса в теле** — «делать таким-то профилем ревью», «взять
|
||||
такой-то агент»: это второй дом для правила выбора и путь понизить требования
|
||||
решением, принятым до проектирования. Снимается.
|
||||
|
||||
## Переносимость
|
||||
|
||||
Скилл независим от **языка программирования, сборки, CI и трекера**: он ничего
|
||||
не знает ни про Go, ни про npm, ни про конкретный багтрекер — задачи для него
|
||||
просто каталог markdown. Текст задач — русский (язык документации проекта);
|
||||
зашита только латиница слага. OpenSpec ему тоже не нужен.
|
||||
|
||||
- **Каталог задач — `docs/tasks`, жёстко.** Цепочки разрешения нет: раскладка
|
||||
канона одинакова во всех проектах, и искать больше нечего. Каталога нет — код
|
||||
3 и вопрос человеку; `init` заводит его **только** когда проект действительно
|
||||
новый, а перевод чужой раскладки делает `av-dev-pm:canon`.
|
||||
- **Настройки живут в `docs/.pm.json`**, ключ `tasks`: секции беклога и имена
|
||||
индексов, если они отличаются от умолчания. Один конфиг на весь канон, а не по
|
||||
одному на каталог.
|
||||
- **Секции беклога** берутся из заголовков `##` индекса как есть; их количество
|
||||
и названия — дело проекта (умолчание `ядро` / `инфра`).
|
||||
|
||||
### Вызов из другого плагина
|
||||
|
||||
`$CLAUDE_PLUGIN_ROOT` раскрывается **только внутри своего плагина**: пайплайн
|
||||
задачи, конвейер ревью и любой другой чужой контекст до `tasks.py` по этой
|
||||
переменной не дотянутся. Мост — **вызов скилла через пространство имён**, а не
|
||||
путь:
|
||||
|
||||
> Чужой контекст зовёт `Skill av-dev-pm:tasks` и называет, что нужно сделать
|
||||
> («закрой задачу `<слаг>`, реализована»). Скилл разрешает свой
|
||||
> `$CLAUDE_PLUGIN_ROOT` сам. Путь наружу не выносится вовсе.
|
||||
|
||||
Плагина в проекте нет — вызов не разрешится, и вызывающий **не выдумывает путь и
|
||||
не правит индекс руками**, а сообщает в докладе, что учёт задач остаётся за
|
||||
владельцем.
|
||||
|
||||
## Слоты проекта
|
||||
|
||||
Почти всё, что скиллу нужно знать о проекте, отвечает канон структурой: куда
|
||||
переезжает суть реализованной задачи — `openspec/specs`, `adr/`, архив change;
|
||||
какие в проекте оракулы — семантика гейта в `CLAUDE.md`. Отдельными слотами
|
||||
остаётся то, чего из раскладки не вывести. **Проект дописывает в `CLAUDE.md`**:
|
||||
|
||||
1. **Что такое «сделана»** — чем задача выполняется (пайплайн проекта) и что
|
||||
входит в его определение готовности. Скилл требует лишь **форму**: пайплайн
|
||||
проекта пройден + критерии приёмки проверены поимённо.
|
||||
2. **Что считается необратимым** и потому спрашивается у человека всегда
|
||||
(деплой, выкладка наружу, удаление или перезапись данных).
|
||||
|
||||
Ни того ни другого скилл не угадывает: не нашёл — спрашивает пользователя, а не
|
||||
подставляет умолчание.
|
||||
|
||||
## Общее для всех сценариев
|
||||
|
||||
- **Развилки — пользователю.** Через `AskUserQuestion`, с уже сформулированным
|
||||
предварительным суждением (**рекомендация — первым вариантом**). Что выкинуть,
|
||||
под какую цель отнести, какая рамка идеи верна — решение пользователя. Слаг,
|
||||
формулировка, порядок строк в индексе — механика, делаем сами.
|
||||
- **Не больше трёх вопросов за раз.** Пачка длиннее трёх тяжела для ответа;
|
||||
решений больше — веди **несколько итераций** диалога по ≤3, а не один
|
||||
перегруженный запрос. Между итерациями применяй уже решённое.
|
||||
- **Границы покрытия в отчёте.** Любая сессия разбора, штурма или интейка
|
||||
заканчивается строкой «просмотрено N из M, не трогали — …». Отчёт без неё
|
||||
сообщает «беклог разобран», не сообщая, какая его часть осталась нетронутой.
|
||||
- **Ничего не удаляем молча.** Файл исчезает только через `close` — `--reason`
|
||||
(ушла без реализации) или `--implemented` (реализована). Прямого `rm` нет.
|
||||
- **Слаги английские**, kebab-case, не транслит: `tie-break-equal-completeness`,
|
||||
а не `taj-brejk-pri-ravnoj-polnote`. Заголовки, тела и хуки — русские.
|
||||
|
||||
## Чего этот скилл не делает
|
||||
|
||||
Не пишет код, не заводит спеки и предложения об изменении, не берёт задачу в
|
||||
работу — этим занимается пайплайн проекта. Не ведёт спринт и не проводит сессию
|
||||
между спринтами — это `session`. Не решает за пользователя, что важно. Не
|
||||
переоформляет существующие задачи «заодно»: правится то, чего касается операция.
|
||||
@@ -0,0 +1,122 @@
|
||||
# Адаптация каталога задач
|
||||
|
||||
Проект, где задачи уже как-то ведутся, и из имеющегося материала **выводится**
|
||||
заполненный каталог задач: цели, задачи, кладбище, индексы. Операция разовая —
|
||||
после неё проект живёт скиллами `tasks` и `session`.
|
||||
|
||||
**Это часть приведения проекта к канону.** Раскладку `docs/` целиком ведёт скилл
|
||||
`av-dev-pm:canon`; он же зовёт этот сценарий на шаге «каталог задач», потому что
|
||||
форматом задач владеет `tasks`, а не `canon`. Отдельно сценарий вызывается,
|
||||
когда переводить надо **только** задачи.
|
||||
|
||||
Вход какой угодно: старая раскладка `av-dev-backlog` (индекс `README.md`,
|
||||
кладбище `CLOSED.md`, приоритеты секциями, транслитные слаги, файлы рядом с
|
||||
индексом), `TODO.md`, россыпь заметок, раздел «планы» в `README.md`, список
|
||||
шагов в плане проекта.
|
||||
|
||||
## Три правила, из которых всё следует
|
||||
|
||||
1. **Сперва карта, потом файлы.** Человеку показывается, что найдено, как
|
||||
разложилось по целям и **что не разложилось**, — и только после подтверждения
|
||||
пишется хоть один файл. Это то же правило, что у интейка находок ревью:
|
||||
массовое заведение записей без подтверждения — самый дорогой отказ, потому
|
||||
что разгребает его потом переоценка.
|
||||
2. **Ничего не терять.** Исходный текст переезжает в тело, хук и причина
|
||||
сохраняются, кладбище переносится строка в строку. Переименование слага —
|
||||
не правка, а **перенос ссылок**: он делается одним проходом вместе с
|
||||
переименованием, иначе останутся битые ссылки, которых никто не проверяет.
|
||||
3. **Что не классифицировалось — назвать поимённо.** Проглоченный пункт
|
||||
выглядит как «всё перенеслось». Список «не разложилось» идёт в доклад
|
||||
целиком, с причиной по каждому пункту.
|
||||
|
||||
## Форма: карта — суждение — запись
|
||||
|
||||
Механику несёт `tasks.py adopt`, суждение — ты. Разделено ровно по границе
|
||||
«машина умеет / не умеет»:
|
||||
|
||||
```
|
||||
tk="$CLAUDE_PLUGIN_ROOT/skills/tasks/scripts/tasks.py"
|
||||
|
||||
python3 $tk adopt scan --from docs/backlog docs/plan.md TODO.md \
|
||||
--target docs/tasks --out tasks-adopt-plan.json # только чтение
|
||||
python3 $tk adopt apply --plan tasks-adopt-plan.json \
|
||||
--refs docs openspec CLAUDE.md README.md # запись
|
||||
```
|
||||
|
||||
`scan` ничего не пишет, кроме карты: он распознаёт раскладку, собирает записи,
|
||||
хуки, причины, кладбище, помечает похожее на транслит и на открытый вопрос в
|
||||
прозе, и **называет поимённо** то, что не разложилось. `apply` пишет каталог
|
||||
целиком одним проходом и чинит перекрёстные ссылки.
|
||||
|
||||
Между ними — твоя работа, которую машина не сделает:
|
||||
|
||||
- **английские слаги.** Перевести `taj-brejk-pri-ravnoj-polnote` в
|
||||
`tie-break-equal-completeness` может только тот, кто понимает смысл. `scan`
|
||||
честно говорит: проверить надо **все** слаги, признаки транслита — эвристика;
|
||||
- **цели.** Шаги плана — готовые цели **линии** (порядок и обоснование у них уже
|
||||
есть); тематические скопления задач — **кусты** («прочность слияния», «журнал
|
||||
и пересборка»). Предлагаешь ты, назначает человек;
|
||||
- **что вообще не задача.** Обоснование порядка шагов, абзац прозой, заголовок
|
||||
раздела — это не пункты беклога, и они уходят в «не разложилось» с причиной.
|
||||
|
||||
## Порядок
|
||||
|
||||
1. **Осмотрись.** Где лежат задачи, план, заметки. Каталог задач по канону —
|
||||
всегда `docs/tasks`. Секции беклога (`--sections`) — по умолчанию
|
||||
`ядро,инфра`; если у проекта деление другое по существу, оно называется
|
||||
здесь, а не подгоняется под умолчание, и уезжает в `docs/.pm.json`.
|
||||
2. **`adopt scan`** по всем источникам разом. Один прогон, одна карта: два
|
||||
прохода дадут два несогласованных состояния.
|
||||
3. **Заполни карту**: `slug` (английский), `section`, `goal` у каждой записи;
|
||||
список `goals` — из шагов плана и из кустов. Закрытый шаг плана целью не
|
||||
заводится. Пустой `goal` — законный исход только у идеи.
|
||||
4. **Покажи человеку карту** через `AskUserQuestion`, ≤3 вопроса за итерацию,
|
||||
рекомендация первым вариантом. Показывается: сколько записей, предлагаемые
|
||||
цели (линия и кусты) с обоснованием, спорные отнесения, список «не
|
||||
разложилось». Массовые механические решения (слаги, порядок строк) не
|
||||
выносятся — это механика.
|
||||
5. **`adopt apply`.** `--refs` перечисляет **всё**, где могут стоять ссылки на
|
||||
слаги: документация, архив изменений, `CLAUDE.md`, `README.md`. Скрипт
|
||||
посчитает и покажет, сколько ссылок поправлено и по каким слагам.
|
||||
6. **`tasks.py check`** и доклад.
|
||||
|
||||
`apply` отказывается писать поверх живого каталога и проверяет карту целиком
|
||||
**до** первой записи: неверная секция, дубль слага, цель, которой нет в карте —
|
||||
всё это отказ до того, как на диске появился хотя бы один файл.
|
||||
|
||||
## Переходное состояние — объявляется, а не заминается
|
||||
|
||||
Сразу после адаптации задачи в большинстве своём **не готовы к взятию**: у них
|
||||
нет критериев приёмки, а у части может не быть цели. Это нормально, но обязано
|
||||
быть названо, иначе следующий агент примет пустой беклог за поломку.
|
||||
|
||||
`apply` печатает состояние по факту: сколько задач без цели (это **ошибки**
|
||||
`check`) и сколько без критериев (`check` их ошибкой не считает, но `sprint
|
||||
take` такую задачу не возьмёт). Закрывается это **порциями переоценки** — шаг 3
|
||||
скилла `session`, 5–8 задач за порцию: проставить цели, превратить «готово,
|
||||
когда» в критерии с оракулами, вынуть вопросы из прозы в раздел «Вопросы».
|
||||
|
||||
Готовность к первому спринту — не «`check` зелёный», а «есть 2–5 критериев хотя
|
||||
бы у набора под одну цель».
|
||||
|
||||
## Чего адаптация не делает
|
||||
|
||||
- **Не удаляет источники.** Старый каталог остаётся на месте: сверить и убрать —
|
||||
дело человека, удалять чужое молча нельзя. В доклад идёт готовая команда.
|
||||
- **Не переписывает подписи ссылок.** `[docs/backlog](docs/tasks/BACKLOG.md)` —
|
||||
цель поправлена, текст остался; это правится глазами, и таких мест немного.
|
||||
- **Не сочиняет критерии приёмки и не придумывает цели**, которых в материале
|
||||
нет. Придуманная цель хуже отсутствующей: под неё соберут спринт.
|
||||
- **Не трогает историю.** В коммитах старые слаги остаются, и это нормально.
|
||||
|
||||
## Доклад
|
||||
|
||||
- Источники и что в каждом распознано (раскладка, индекс, кладбище, секции).
|
||||
- Сколько записей перенесено, сколько целей заведено (линия / кусты) и откуда
|
||||
каждая выведена.
|
||||
- **Переименования**: сколько слагов, сколько ссылок поправлено и в скольких
|
||||
файлах — числом, а не «поправлены ссылки».
|
||||
- **Не разложилось**: поимённо, с причиной.
|
||||
- Переходное состояние: сколько задач без цели, сколько без критериев, чем и за
|
||||
сколько порций закрывается.
|
||||
- `tasks.py check` — результат строкой.
|
||||
@@ -0,0 +1,102 @@
|
||||
# Задачи из аудита и ревью
|
||||
|
||||
Ревью и аудиты — код-ревью, архитектурный проход, аудит безопасности, любой
|
||||
разбор другим агентом — порождают находки, часть которых становится задачами.
|
||||
Это отдельный интейк со своей опасностью, **зеркальной** интейку из диалога.
|
||||
|
||||
- Интейк из диалога грешит переполнением: из одной мысли рождается пять файлов.
|
||||
- Интейк из ревью грешит сваливанием: сорок сырых находок превращаются в сорок
|
||||
файлов. Беклог раздувается, а следующая переоценка склеивает их обратно.
|
||||
|
||||
Защита от сваливания — та же, что в самом ревью: **кластеризация по причине, а
|
||||
не файл-на-находку.** Если у ревью был триаж — половина работы уже сделана, бери
|
||||
его выход. Если нет — триажируй сам, прежде чем заводить.
|
||||
|
||||
## Находка агента — не задача
|
||||
|
||||
Мнение агента — **гипотеза, пока у неё нет свидетельства** (падающий тест,
|
||||
воспроизводимый шаг, положение гайда). Согласие нескольких находок само по себе
|
||||
достоверность не повышает: это один источник, высказавшийся несколько раз.
|
||||
|
||||
Отсюда фильтр входа, поверх обычного «не делаем сейчас + пожалеем о потере»:
|
||||
|
||||
- **Находка со свидетельством**, отложенная к исполнению → **задача**.
|
||||
Свидетельство и последствие переносим в тело — это её «почему», то самое, что
|
||||
переживает запись.
|
||||
- **Находка без свидетельства / низкой уверенности** → **идея** (`[idea]`), а не
|
||||
задача. Её судьба — штурм, где либо найдётся подтверждение, либо она уедет в
|
||||
`REJECTED.md`.
|
||||
- **Уже починено по ходу ревью** → **ничего**. Починенное не заводим.
|
||||
- **Развилка, решённая при ревью** → ничего; решённая «потом» → задача с
|
||||
вопросом в разделе «Вопросы» и тегом `question`.
|
||||
|
||||
## Порядок
|
||||
|
||||
1. **Возьми выход триажа, а не сырые находки.** Сырой отчёт — это симптомы до
|
||||
дедупликации; в нём одна причина размазана по нескольким строкам.
|
||||
2. **Кластеризуй по причине.** Пять находок об одном отсутствующем инварианте —
|
||||
одна задача, а не пять. Класс мелочи (nits, косметика) — **один пакетный
|
||||
файл** со списком пунктов, а не файл на каждую запятую.
|
||||
3. **Дедуп против живых задач и `REJECTED.md`.** Аудит переоткрывает уже
|
||||
заведённое и уже выкинутое. Нашлось среди живых — дописываем находку в
|
||||
существующий файл. Нашлось в `REJECTED.md` — это сигнал: причина отказа могла
|
||||
устареть, выноси пользователю, а не заводи молча заново.
|
||||
4. **Разложи по целям.** У каждой заводимой задачи должен быть `goal:<слаг>`.
|
||||
Половина находок ревью не служит ничему из линии продукта — их цель это
|
||||
**куст** («прочность слияния», «журнал и пересборка», «наблюдаемость»).
|
||||
Подходящего куста нет — заведи его целью (`add --type goal --section кусты`)
|
||||
в том же проходе: без цели задача не попадёт ни в один спринт, а значит не
|
||||
будет сделана никогда.
|
||||
5. **Покажи карту до создания файлов.** Кластер → задача / идея / строка в
|
||||
пакетный файл / уже заведено / отброшено, и под какую цель — пачкой через
|
||||
`AskUserQuestion`. Это тот же барьер, что и «три кандидата» в интейке из
|
||||
диалога: массовое заведение файлов без подтверждения — ровно тот отказ, ради
|
||||
которого интейк из ревью и выделен. Дешёвая мелочь по явному согласию может
|
||||
заводиться и без поштучного вопроса — но карта пользователю предъявляется
|
||||
всё равно.
|
||||
6. **Заводи утверждённое** через `tasks.py add`, с двумя добавками:
|
||||
- **тег партии** — `--tag review-ГГГГ-ММ-ДД` (или `audit-<тема>`), чтобы весь
|
||||
заход разбора поднимался одной командой `list --tag …`;
|
||||
- **провенанс в теле** — кто нашёл, каким проходом, с каким свидетельством.
|
||||
Без него через месяц не отличить проверенную находку от догадки.
|
||||
7. `tasks.py check`.
|
||||
|
||||
## Куда девается серьёзность, если приоритетов нет
|
||||
|
||||
Приоритетов нет, и отображать серьёзность некуда — но **выкидывать её нельзя**.
|
||||
Правило замены:
|
||||
|
||||
- **тяжёлая находка со свидетельством** → задача под ту цель, которой она
|
||||
угрожает, и **кандидат в ближайший набор**: серьёзность здесь превращается в
|
||||
довод при выборе цели следующего спринта, а не в уровень в файле. Довод
|
||||
записывается причиной в мета-строке (`--reason`), иначе к моменту набора его
|
||||
никто не вспомнит;
|
||||
- **находка, ломающая уже идущий спринт**, — не интейк вовсе: см. правило
|
||||
вторжения в скилле `session`. В беклог она падает, только если врываться не
|
||||
положено;
|
||||
- **низкая уверенность или нет свидетельства** → идея;
|
||||
- **мелочь** → строка в пакетный файл;
|
||||
- **уже починено / развилка решена сейчас** → ничего.
|
||||
|
||||
Словарей серьёзности много, и отображать их механически не на что: при сомнении
|
||||
— вопрос пользователю, а не догадка.
|
||||
|
||||
## Поимённая сверка
|
||||
|
||||
Интейк считается выполненным, только если **каждая** находка триажа получила
|
||||
исход: слаг заведённой задачи, ссылку на существующую, строку пакетного файла
|
||||
или запись «не заведена: причина». Нулевой урожай при непустом отчёте триажа
|
||||
виден сразу — и это единственный способ отличить «находок не было» от «не стал
|
||||
заводить». Список составляет не тот, кто отчитывается о заведении.
|
||||
|
||||
Границы покрытия отчёта — то, что ревью проверить **не смогло**, — не находки и
|
||||
в задачи не идут: у них нет предмета. Их место в докладе, не в беклоге.
|
||||
|
||||
## Доклад
|
||||
|
||||
- Источник (какое ревью/аудит, сколько находок на входе).
|
||||
- Свёрнуто в задачи: N кластеров из M находок, со слагами, целями и тегом партии.
|
||||
- Что не заведено и почему: починено инлайн, уже заведено, ушло в идеи, в
|
||||
`REJECTED.md`.
|
||||
- Поимённая сверка: находок на входе N, исход есть у N.
|
||||
- `tasks.py check`.
|
||||
@@ -0,0 +1,82 @@
|
||||
# Декомпозиция и мозговой штурм
|
||||
|
||||
Обе операции превращают одну запись в несколько (или в ноль). Разница во входе:
|
||||
декомпозиция дробит **готовую задачу или эпик**, штурм прорабатывает **идею**,
|
||||
которая ещё не задача.
|
||||
|
||||
## Тест декомпозиции
|
||||
|
||||
Задачу можно дробить, только если части удовлетворяют **обоим** условиям:
|
||||
|
||||
1. **Мерджатся независимо.** Часть Б не требует, чтобы часть А была уже влита.
|
||||
Есть порядок «сперва А, потом Б, иначе не собрать» → это не декомпозиция, а
|
||||
план реализации: шаги остаются **внутри одного файла**.
|
||||
2. **Каждая даёт видимую пользу.** Часть, полезная только в комплекте с другой,
|
||||
— не самостоятельная задача. Пользу проверяй тестом «готова к взятию»
|
||||
(task-format): что станет наблюдаемо иначе именно от этой части и какие у неё
|
||||
собственные критерии приёмки.
|
||||
|
||||
Не проходит хотя бы одно — **не дроби**. Ложная декомпозиция плодит файлы,
|
||||
которые нельзя взять поодиночке, и переоценка потом склеивает их обратно.
|
||||
|
||||
**Цель наследуется.** Все части несут `goal:` родителя: декомпозиция не меняет
|
||||
того, чему работа служит. Если у части цель другая — это признак, что дробили не
|
||||
по той границе, либо что часть вообще из другой работы.
|
||||
|
||||
## Что делать с родителем
|
||||
|
||||
После разделения родитель **не остаётся** третьей висящей строкой:
|
||||
|
||||
- части полностью замещают его → `close <slug> --reason "разложена на a, b"`.
|
||||
`REJECTED.md` здесь — не «выкинули», а именно тот след, что переживает запись:
|
||||
через квартал вопрос «куда делась задача X» отвечается строкой со ссылками на
|
||||
наследников, а не археологией git;
|
||||
- родитель осмыслен как зонтик → `edit <slug> --type epic`, тело — ссылки на
|
||||
задачи-части, своих шагов у него нет. **Эпик не берётся в спринт** и живёт
|
||||
ровно до тех пор, пока не закрыта последняя часть.
|
||||
|
||||
Зонтик, который перестал быть временным и описывает направление, а не работу, —
|
||||
это уже **цель**, а не эпик. Тип на месте не меняется (цель живёт в другом
|
||||
индексе): заводится `[goal]` в `PLAN.md`, задачи получают `--goal <новый слаг>`,
|
||||
эпик закрывается с причиной-ссылкой.
|
||||
|
||||
## Когда декомпозиция случается посреди спринта
|
||||
|
||||
Задача, которая **переросла в эпик**, распознаётся до того, как под неё заведено
|
||||
предложение об изменении: иначе его придётся выбрасывать. Она помечается
|
||||
`[epic]`, выходит из набора (`sprint drop … --reason "переросла в эпик"`), уходит
|
||||
на декомпозицию, а спринт продолжается остальными. Части заводятся сразу, но в
|
||||
текущий набор **не добавляются** — набор заморожен.
|
||||
|
||||
## Мозговой штурм идеи
|
||||
|
||||
Идея (`[idea]`) не проходит тест «готова к взятию»: неясно, что именно делаем.
|
||||
Штурм проясняет — и это **generative-операция, а не applicative**.
|
||||
|
||||
Applicative-штурм («перечисли задачи, следующие из идеи») выдаёт очевидное:
|
||||
перечисляется то, что уже видно в формулировке. Ценное — на уровень выше.
|
||||
|
||||
1. **Сперва — формы, а не задачи.** Предложи **три разные постановки** идеи и
|
||||
назови **компромисс каждой**: что она даёт, чем платит, что оставляет за
|
||||
бортом. Если получилась одна постановка — штурм не состоялся, это
|
||||
applicative.
|
||||
2. **Вынеси формы пользователю** через `AskUserQuestion` с компромиссами. Рамку
|
||||
выбирает он: это продуктовое решение, не механика.
|
||||
3. **Назови цель.** Выбранная форма служит какой-то цели — существующей или
|
||||
новой. Идея, для которой цель не находится, скорее всего уезжает в
|
||||
`REJECTED.md`, а не заводится задачей.
|
||||
4. **Только выбранную форму** дроби по тесту декомпозиции выше и проставь
|
||||
критерии приёмки: без них наследники останутся идеями под другим именем.
|
||||
|
||||
**«Выкинуть» — полноправный исход штурма, а не его неудача.** Проработка, честно
|
||||
показавшая, что пользы нет или она несоразмерна цене, — это результат: идея
|
||||
уезжает с этой самой причиной, и та причина гасит её повторное появление.
|
||||
|
||||
## Доклад
|
||||
|
||||
- Идея/задача на входе, выбранная рамка (для штурма), задачи-наследники со
|
||||
слагами, целями и секциями.
|
||||
- Судьба родителя: удалён / стал эпиком / стал целью / выкинут с причиной.
|
||||
- `tasks.py check` после правок.
|
||||
- Границы покрытия: какие постановки рассмотрены и какие сознательно отброшены —
|
||||
чтобы штурм не пришлось повторять с нуля.
|
||||
@@ -0,0 +1,245 @@
|
||||
# Формат задач, целей и индексов
|
||||
|
||||
Заголовок, мета-строку и строку индекса ставит `tasks.py add` — руками их не
|
||||
пишут. Этот файл описывает, что именно скрипт создаёт и что проверяет `check`;
|
||||
тело задачи (одну фразу, критерии, рамки, контекст) дописывает агент.
|
||||
|
||||
## Файл задачи
|
||||
|
||||
`items/<slug>.md`:
|
||||
|
||||
```markdown
|
||||
# Тай-брейк при равной полноте
|
||||
|
||||
**Секция:** ядро — вышла из спринта: остаток писал нерешённое в журнал · **Хук:** порядок канонических форм берёт меньшее в 96% случаев — для накопительных это систематический недосчёт · **Теги:** goal:merge-robustness, sprint:2026-08-03
|
||||
|
||||
При столкновении точек выигрывает более полная, но при равной полноте побеждает
|
||||
последняя доставка — а она систематически беднее первой.
|
||||
|
||||
## Критерии приёмки
|
||||
|
||||
- повторный прогон свёртки даёт тот же отпечаток состояния — оракул: команда сверки
|
||||
- накопительная метрика за сутки не уменьшается после повторной доставки — оракул: тест
|
||||
- в логе видно, какая из двух точек выиграла и почему — оракул: глазами по логу прогона
|
||||
|
||||
## Рамки
|
||||
|
||||
Схема не трогается; данные только читаются; перезапуск сервиса допустим.
|
||||
|
||||
Связано: решение о канонической форме содержимого.
|
||||
```
|
||||
|
||||
- **Заголовок H1** — он же заголовок строки в индексе, дословно. Тип кодируется
|
||||
префиксом `[goal]` / `[idea]` / `[epic]`; обычная задача — без префикса.
|
||||
Отдельного поля типа **нет**: два места для одного факта разъезжаются, а
|
||||
префикс виден прямо в индексе, где и принимается решение «брать или не брать».
|
||||
- **Мета-строка** — первая непустая строка после заголовка. Обязательна секция,
|
||||
причина после тире желательна (именно она объясняет, почему задача здесь
|
||||
оказалась — в том числе «вышла из спринта: …»), хук и теги опциональны. Поля
|
||||
разделяются ` · `, порядок свободный. `·` — служебный разделитель: в тексте
|
||||
причины и хука его быть не должно.
|
||||
- **Хук живёт здесь, а не только в индексе.** Строка индекса его повторяет и
|
||||
производна от него: `check` сверяет, `check --fix` восстанавливает пропавшую
|
||||
строку **вместе с хуком**. Пока хук лежал только в индексе, штатная починка
|
||||
дрейфа теряла его молча и навсегда — а хук это единственное, по чему задачу
|
||||
выбирают, не открывая.
|
||||
- **Тело** — одна фраза «что станет наблюдаемо иначе», критерии приёмки, рамки,
|
||||
контекст, ссылки. Пишется на языке документации проекта.
|
||||
|
||||
Тело — не план реализации и не спецификация: принятое и реализованное переезжает
|
||||
в документацию проекта, а файл задачи удаляется.
|
||||
|
||||
### Критерии приёмки
|
||||
|
||||
2–5 проверяемых утверждений **списком** `- …`, **у каждого назван оракул**. Не
|
||||
«работает корректно», а «повторный прогон даёт тот же отпечаток — оракул:
|
||||
команда сверки». Это не второе определение готовности, а проектная
|
||||
конкретизация вопроса «по чему видно, что закончено» из теста готовности ниже:
|
||||
там сказано «признак завершённости», здесь — «признак плюс чем проверяется».
|
||||
|
||||
**Что из этого механизировано.** `check` и `sprint take` считают пункты: меньше
|
||||
двух — отказ («— работает» одной строкой больше не проходит), больше пяти —
|
||||
замечание, обычно это признак, что задача крупнее задачи. Наличие оракула
|
||||
проверяется **эвристикой** — словом «оракул» в пункте, — и потому даёт только
|
||||
замечание: настоящий оракул от слова «оракул» машина не отличает, и делать вид,
|
||||
что проверено больше проверенного, хуже, чем не проверять вовсе.
|
||||
|
||||
**У идей критериев нет — именно поэтому они идеи.**
|
||||
|
||||
**Критерии — пол, но расхождение с ними есть дефект критериев.** Если приёмщик
|
||||
видит, что критерии закрыты, а суть задачи не достигнута, он **правит критерии и
|
||||
возвращает задачу исполнителю**, а не держит невидимое сверх-требование. Иначе
|
||||
исполнитель никогда не знает, закончил ли, и мотивирован занижать критерии
|
||||
заранее.
|
||||
|
||||
### Рамки
|
||||
|
||||
Одна строка: чего касаться нельзя, что перезапускается, что считается
|
||||
необратимым, трогается ли схема данных. **Свойства репозитория сюда не пишутся**
|
||||
— номер последней миграции, версия зависимости, хеш: в лежалой задаче они
|
||||
протухают молча и становятся ложной рамкой. Снимок берётся при постановке, а не
|
||||
при заведении.
|
||||
|
||||
### Вопросы
|
||||
|
||||
Неразобранное решение человека живёт разделом `## Вопросы` **плюс тегом
|
||||
`question`**. Раздел без тега или тег без раздела — дрейф, `check` о нём скажет.
|
||||
|
||||
**Судит факт, а не метка.** Отказ во взятии даёт **непустой раздел «Вопросы»**,
|
||||
независимо от того, стоит ли тег: иначе забывший тег проходил бы, а поставивший
|
||||
спотыкался — стимул ровно обратный записанному правилу. Тег производен: он нужен
|
||||
отбору снаружи файла (`list --questions`, `list --tag question`), и его
|
||||
отсутствие при непустом разделе — замечание, а не лазейка. Тег без раздела тоже
|
||||
отказ, но с другим советом: либо вопрос записан не туда, либо тег пора снять.
|
||||
|
||||
Ответ записывается в тело, тег снимается `edit <slug> --rm-tag question`, а хук
|
||||
переписывается: «Решено: …» на вопрос «почему это лежит в беклоге» уже не
|
||||
отвечает.
|
||||
|
||||
## Файл цели
|
||||
|
||||
```markdown
|
||||
# [goal] Прочность слияния
|
||||
|
||||
**Секция:** кусты · **Теги:** decomposed
|
||||
|
||||
Ради чего: точки из разных доставок сходятся в один часовой объект, и сегодня
|
||||
исход столкновения зависит от порядка доставки, а не от содержания.
|
||||
|
||||
## Завершение
|
||||
|
||||
Достигнута, когда исход слияния не зависит ни от порядка, ни от времени
|
||||
доставки, и это подтверждено повторным прогоном на живом корпусе.
|
||||
```
|
||||
|
||||
- **Задачи цели здесь не перечисляются.** Перечень даёт
|
||||
`tasks.py list --goal <слаг>`; хранимый список стал бы третьим индексом и
|
||||
поехал бы на первой же закрытой задаче.
|
||||
- **Раздел «Завершение»** — то, по чему видно, что цель достигнута.
|
||||
- **Тег `decomposed`** отличает «цель ещё не разобрана» от «все её задачи
|
||||
закрыты» — два состояния, у которых снаружи один и тот же признак: задач нет.
|
||||
Пометка именно **тегом**, а не строкой в теле: только так она проверяется.
|
||||
`check` требует его у цели без задач, `check --fix` сам ставит его цели, у
|
||||
которой задачи есть, а цель с тегом и без задач — прямое приглашение закрыть.
|
||||
- Цель живёт в `PLAN.md` и **никогда** — в `BACKLOG.md` или `SPRINT.md`.
|
||||
|
||||
## Слаг
|
||||
|
||||
Латиница и цифры, kebab-case, без ведущих, хвостовых и двойных дефисов
|
||||
(`foo-bar`, не `-foo`, `a--b`). Именуется **по сути, а не по текущей
|
||||
формулировке**: заголовок будет переписан при переоценке, а слаг стоит в ссылках
|
||||
из других задач, коммитов и черновиков. **Транслита не заводим** —
|
||||
`tie-break-equal-completeness`, а не `taj-brejk-pri-ravnoj-polnote`: транслит
|
||||
нечитаем для того, кто ищет по смыслу, и не сокращается.
|
||||
|
||||
Переименование слага — не правка, а перенос ссылок: делается одним атомарным
|
||||
проходом по всем местам, где слаг упомянут, иначе останутся битые ссылки,
|
||||
которых никто не проверяет.
|
||||
|
||||
## Индексы
|
||||
|
||||
Строка везде одной формы:
|
||||
|
||||
```markdown
|
||||
- [Заголовок дословно](items/slug.md) — хук
|
||||
```
|
||||
|
||||
Хук отвечает на «почему это лежит в беклоге» одним предложением: состояние,
|
||||
остаток, боль. Пересказ первого абзаца бесполезен — он уже есть по ссылке.
|
||||
|
||||
| Файл | Что отвечает | Секции |
|
||||
| --- | --- | --- |
|
||||
| `PLAN.md` | какие есть цели, в каком порядке идёт линия и почему | линия (упорядоченная) и кусты |
|
||||
| `BACKLOG.md` | что **можно взять** — только задачи | секции проекта (по умолчанию ядро/инфра) |
|
||||
| `SPRINT.md` | какая цель и какой набор под неё | одна: «Набор» |
|
||||
| `REJECTED.md` | что ушло без реализации и почему | — |
|
||||
|
||||
Секции — **единственные заголовки `##` в индексе**: любой другой `##` в
|
||||
преамбуле проверка сочтёт секцией. Внутри секции беклога порядок значения не
|
||||
имеет — порядка в беклоге нет вовсе.
|
||||
|
||||
**Секции «блокеры» среди них нет.** Блокер — состояние, а не полка: он живёт до
|
||||
ответа человека, а следы остаются вопросами в файлах задач распущенного спринта.
|
||||
Постоянно пустая секция со старой семантикой «разбираются пачками» противоречила
|
||||
бы правилу «эскалируем немедленно», поэтому `init` её не заводит, а `check`
|
||||
говорит о ней в чужом беклоге. Переезжаешь с такой секцией — удали её. В **линии** плана порядок значим и
|
||||
обосновывается прозой; двигают строку `move <slug> --section линия --after
|
||||
<другой>`.
|
||||
|
||||
Индексы **производны**: расходятся с файлом — правим индексы (`check --fix`).
|
||||
Строку руками не пишут.
|
||||
|
||||
Отсюда же ответ на «а если оборвётся посередине». Мутация сперва проверяет всё
|
||||
и складывает правки, и только потом пишет: сначала все временные файлы, потом
|
||||
переименования подряд. Полной транзакции на несколько файлов файловая система не
|
||||
даёт, но окно сжато до цепочки переименований, а **всё, что в нём может
|
||||
разъехаться, — производное**: файлы целы, индексы восстанавливает `check --fix`.
|
||||
Поэтому отказ на второй задаче из пяти не оставляет первую переписанной при
|
||||
нетронутых индексах.
|
||||
|
||||
`SPRINT.md` и есть артефакт заморозки: без него набор существует только в
|
||||
контексте сессии, и нарушение заморозки ненаблюдаемо.
|
||||
|
||||
## `REJECTED.md`
|
||||
|
||||
Туда уходит задача, покинувшая беклог **без реализации**. Строку пишет
|
||||
`tasks.py close --reason`, а `check` следит за форматом:
|
||||
|
||||
```markdown
|
||||
- 2026-07-23 `versii-kachestvo-repaki` — Версии и качество одного тайтла.
|
||||
Причина: калибровка болей — не боль, ни разу не возникло за полгода.
|
||||
Была секция: инфра.
|
||||
```
|
||||
|
||||
Реализованные сюда не попадают: у них остаётся коммит и документация. У
|
||||
выкинутой не остаётся ничего — и через квартал она возвращается тем же текстом.
|
||||
Это первое место, куда смотрит дедупликация при заведении.
|
||||
|
||||
Запись не запрещает завести задачу заново: изменился контекст — заводим и
|
||||
ссылаемся на строку, объясняя, что изменилось.
|
||||
|
||||
## Теги
|
||||
|
||||
Единственный механизм разметки, потому что `list --tag` уже умеет отбирать по
|
||||
ним порцию разбора. Отдельных полей мета-строки под это не заводим.
|
||||
|
||||
- `goal:<слаг>` — цель, которой служит задача. Обязателен: задача без цели не
|
||||
попадёт ни в один спринт.
|
||||
- `question` — в файле есть неразобранный раздел «Вопросы».
|
||||
- `sprint:<слаг>` — задача заведена в этом спринте; по нему отбирается первая
|
||||
порция разбора («урожай спринта»). **Ставится сам**: слаг спринта заводит
|
||||
`sprint start` (по умолчанию — дата начала, он же пишется в `SPRINT.md`), и
|
||||
`add` при открытом спринте помечает заводимое. Тег, который надо помнить
|
||||
ставить руками, не ставится никогда — а на нём висит правило «первая порция
|
||||
разбора — урожай прошедшего спринта».
|
||||
- `decomposed` — на цели: разложена на задачи (см. «Файл цели»).
|
||||
|
||||
Отбор — `list --tag a,b`: перечисленные через запятую теги требуются **все
|
||||
сразу** (это И, не ИЛИ). Тег, которого нет ни у одной задачи, `list` называет
|
||||
вслух: молчаливый ноль читается как «таких задач нет», а чаще это опечатка.
|
||||
|
||||
Свои теги проект заводит свободно (партия ревью `review-ГГГГ-ММ-ДД`, тема,
|
||||
источник) — словарь не фиксирован. В индексы теги не выносим: индексы
|
||||
производны, отбор делает `list --tag`, а не глаза.
|
||||
|
||||
## Тест «готова к взятию»
|
||||
|
||||
Задача готова, если из файла отвечаются три вопроса:
|
||||
|
||||
1. **Что станет наблюдаемо иначе**, когда она сделана — снаружи: пользователю,
|
||||
владельцу сервиса или разработчику. «Отрефакторить X» — не ответ; «перестанет
|
||||
ломаться Y при Z» — ответ.
|
||||
2. **По чему видно, что закончено** — критерии приёмки с оракулами.
|
||||
3. **Какой цели она служит** — тег `goal:` и одна строка «почему именно этой».
|
||||
|
||||
Не отвечается первый или второй вопрос → это **идея** (`[idea]`), её место в
|
||||
штурме. Не отвечается третий → либо цель есть и не проставлена, либо задача не
|
||||
служит ничему — тогда её не надо заводить.
|
||||
|
||||
Отвечается всё, но задача не делается одним заходом и не мерджится целиком →
|
||||
**эпик** (`[epic]`), сперва декомпозиция. Эпик временен и исчезает после
|
||||
разбора; цель (`[goal]`) постоянна — не путать.
|
||||
|
||||
Тест применяется при заведении и при переоценке. К старым задачам, которых
|
||||
операция не касается, задним числом не применяется — беклог не переоформляют
|
||||
«заодно».
|
||||
Executable
+2421
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user