Compare commits
27
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
12b77c3393
|
||
|
|
12882911a9
|
||
|
|
63ba36d71d
|
||
|
|
15dba79993
|
||
|
|
c1890d9e71
|
||
|
|
4354cc4146
|
||
|
|
df5af47dc3
|
||
|
|
4a56753f0b
|
||
|
|
3653c5cff5
|
||
|
|
a73eedb893
|
||
|
|
1bce854535
|
||
|
|
ee53ef8af8
|
||
|
|
53cf6baedf
|
||
|
|
c5e6883461
|
||
|
|
c3828b3713
|
||
|
|
872732989a
|
||
|
|
c6be879831
|
||
|
|
c91492e3f0
|
||
|
|
e408c51ac1
|
||
|
|
f40e0cd7bb
|
||
|
|
fdadfb65ac
|
||
|
|
9453a218d1
|
||
|
|
f0dd8f70c1
|
||
|
|
1f31ac6afd
|
||
|
|
00ddfb0dde
|
||
|
|
86e22d932c
|
||
|
|
c669215fc8
|
@@ -6,14 +6,19 @@
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"name": "av-dev-pm",
|
||||
"source": "./av-dev-pm",
|
||||
"description": "Управление продуктом: канон документов проекта, задачи и цели вместо приоритетов, спринт под одну цель с заморозкой набора, старт проекта интервью по брифу и приведение существующего к канону. Ничего не выполняет сам и никакого пайплайна не требует: задача выполняется чем угодно, а канон описывает документы, из которых конвейер ревью берёт проектную конкретику."
|
||||
"name": "av-dev-docs",
|
||||
"source": "./av-dev-docs",
|
||||
"description": "Документация проекта: канон раскладки (CLAUDE.md плюс docs/ — паспорт, архитектура, схема БД, безопасность, конвенции, разведка, ADR, журнал ревью), роли документов и правило единственного дома. Три операции одной машиной сравнения — check, adopt, upgrade — со скриптом docs.py; заведение нового проекта интервью по брифу; ведение содержимого по ходу разработки: синк после сделанной задачи, ADR промоутом из архивного design.md, записка разведки, запись дефекта в журнал ревью. Смысловое, чего скрипт не видит, судит скилл healthcheck двумя агентами разом — doc-consistency (документы между собой и с openspec) и doc-code-drift (факты против кода); язык документов вычитывает агент doc-wording. Задач не ведёт — это плагин av-dev-tasks, и он опционален."
|
||||
},
|
||||
{
|
||||
"name": "av-dev-pipeline",
|
||||
"source": "./av-dev-pipeline",
|
||||
"description": "Проведение задачи через цикл SDD и конвейер ревью с обязательным триажем, плюс прогон нескольких задач разом. Требует OpenSpec. Задача принимается и обычным текстом; плагин av-dev-pm опционален — он даёт документы канона для проходов ревью и учёт задач, без него прогон деградирует поразрядно и говорит об этом."
|
||||
"name": "av-dev-tasks",
|
||||
"source": "./av-dev-tasks",
|
||||
"description": "Задачи и цели каталогом markdown-файлов: одна запись — файл в items/ плюс строка ровно в одном индексе, у записи тип (goal, feature, fix, chore, research), и тип решает, каких разделов она требует и что с ней можно делать. Заведение из диалога с фильтром и дедупом, разбор находок ревью в задачи, декомпозиция на независимо полезные части, гигиена полей, согласованность индексов скриптом tasks.py. Приоритет — явный порядок строк в беклоге, и расставляет его скилл groom: интерактивный разбор на два вопроса, что сейчас самое важное и что перестало быть важным. Готовность записи к работе проверяет команда ready. Записи вычитывают два прохода: task-form (форма записи) и task-wording (язык). Канон документов ведёт плагин av-dev-docs, он опционален. Задач не выполняет — этим занимается конвейер проекта."
|
||||
},
|
||||
{
|
||||
"name": "av-dev-code",
|
||||
"source": "./av-dev-code",
|
||||
"description": "Решение одной задачи от постановки до закрытия скиллом resolve — полный цикл Spec Driven Development с двумя плановыми остановками: чекпоинт вариантов у исследовательской задачи и чекпоинт с объяснением человеческим языком после ревью дизайна у всякой. Между ними работа идёт без согласований. Плюс конвейер ревью с детерминированным гейтом, сверкой со спеками, враждебными постановками, эксплуатационным постмортемом и обязательным триажем. Требует OpenSpec и сам его заводит скиллом openspec. Задача принимается и обычным текстом. Плагины av-dev-docs и av-dev-tasks опциональны: первый даёт документы канона, из которых проходы читают проектную конкретику, второй — учёт задач; без них прогон деградирует поразрядно и называет это строкой."
|
||||
},
|
||||
{
|
||||
"name": "av-dev-git",
|
||||
|
||||
+520
@@ -3127,3 +3127,523 @@ JJJ): у профиля обязан быть один правильный от
|
||||
точно говорит, могла ли измениться дорогая величина. Так дорогая проверка
|
||||
остаётся редкой и при этом не забытой.
|
||||
|
||||
|
||||
## 49. Дом общего правила вышел из плагина (2026-08-09)
|
||||
|
||||
**АЕАКА. Язык уехал в `shared/`, потому что общее правило не может принадлежать
|
||||
половине.** Дом языка лежал в `av-dev-pm/skills/canon/references/language.md` —
|
||||
внутри одного скилла одного плагина. Пока плагин был один, это читалось как «дом
|
||||
рядом с главным потребителем». Разделение на самодостаточные `docs` и `tasks`
|
||||
превращает то же место в утверждение, что язык принадлежит канону: плагин задач,
|
||||
поставленный без канона, потерял бы правила письма вместе с ним. Дом переехал в
|
||||
`shared/` и не принадлежит ни одному плагину, а плагины везут дословные копии.
|
||||
Самодостаточность держится **копией, а не ссылкой**: `shared/` нужен этому
|
||||
репозиторию, а не установленному плагину.
|
||||
|
||||
**АЕАКБ. Устав вычитки стал копией целиком, а не четырьмя таблицами из десяти.**
|
||||
`doc-wording` копировал из дома англицизмы, словарь, жаргон и порог правки —
|
||||
четыре блока; девять правил он излагал своими словами, и эти слова с домом никто
|
||||
не сверял. Там дрейф и копился молча: в доме правило «одна мысль — одно
|
||||
предложение» требовало выносить придаточное, в уставе — не резать причинную
|
||||
связь, и каждая версия выглядела полной. Теперь блок один, `язык-правила`, и
|
||||
берётся он целиком. Условие переезда: текст правил написан безлично, а всё,
|
||||
обращённое к проходу («пиши так-то», «про это молчи»), вынесено из блока в свой
|
||||
раздел устава. **Правило принадлежит дому, способ доложить о нём — уставу.**
|
||||
|
||||
**АЕАКВ. `порог-правки` остался отдельным блоком, и это следствие разметки, а не
|
||||
вкуса.** Его берёт `task-form`, который правил языка не проверяет вовсе.
|
||||
Вложенных блоков `copies.py` не знает — лежи порог внутри `язык-правила`, забрать
|
||||
его отдельно было бы нечем, и `task-form` вёз бы весь устав чужого прохода.
|
||||
Разрез домов идёт **по потребителю, а не по теме**: три блока вместо одного
|
||||
стоят двух лишних маркеров и снимают ложную зависимость.
|
||||
|
||||
### Что из этого следует
|
||||
|
||||
171. **Общее правило не хранится внутри одного из тех, кто им пользуется.** Пока
|
||||
пользователь один, дом рядом с ним выглядит удобством; со вторым
|
||||
пользователем то же место начинает утверждать, что правило принадлежит
|
||||
первому.
|
||||
172. **Пересказ своими словами — это копия, которую никто не сверяет.** Блок,
|
||||
взятый целиком, читается дороже, но расхождение в нём ловит машина;
|
||||
сокращённое изложение экономит строки и платит молчаливым дрейфом.
|
||||
|
||||
|
||||
## 50. Вычитка раздвоилась по плагину, а не по правилу (2026-08-09)
|
||||
|
||||
**АЕАКГ. Решение ППП отменено, и отменено не по своей оси.** ППП говорило: агент
|
||||
называется `doc-wording`, а не `task-wording`, потому что правила языка относятся
|
||||
ко всем проектным текстам — документам канона, решениям ADR, запискам разведки,
|
||||
— а не к одним задачам. Утверждение верно и сегодня; оно и есть причина, по
|
||||
которой правила уехали в `shared/`. Но из общности **правила** не следует
|
||||
общность **прохода**: `docs` и `tasks` расходятся самодостаточными плагинами, а
|
||||
самодостаточный плагин не может зависеть от агента соседа. Проходов теперь два,
|
||||
`doc-wording` и `task-wording`, и разведены они **по охвату** — впервые в этом
|
||||
репозитории: и `task-form` против вычитки, и `doc-consistency` против
|
||||
`doc-code-drift` разведены по глубине.
|
||||
|
||||
**АЕАКД. Разрез по охвату дублирует устав, и потому весь общий текст стал
|
||||
домом.** Два прохода судят по одним и тем же девяти правилам; отличаются они
|
||||
входом, соседями по границе и тем, чем подставляется находка — командой `edit` у
|
||||
задач, редактором у документов. Написать уставы порознь значило бы завести ровно
|
||||
тот дрейф, который днём раньше нашёлся внутри самого `doc-wording`. Общими
|
||||
домами стали `язык-правила`, `порог-правки` и новый `вычитка-доклад` — форма
|
||||
находки и границы покрытия. Копий в каждом уставе 151 строка, своего непустого
|
||||
текста — 61 у `doc-wording` и 75 у `task-wording`, и это ровно то, чем проходы
|
||||
отличаются: вход, соседи, машинная проверка, способ подстановки.
|
||||
|
||||
**АЕАКЕ. `вычитка-доклад` — контракт прохода, а не правило языка, и лежит он всё
|
||||
равно в `shared/language.md`.** Заводить под пятнадцать строк отдельный файл
|
||||
дороже, чем назвать раздел честно. Признак дома здесь не тема, а **число
|
||||
потребителей больше одного при обязательной дословности**: разойдись два прохода
|
||||
формой доклада, зовущий скилл разбирал бы два формата вместо одного.
|
||||
|
||||
### Что из этого следует
|
||||
|
||||
173. **Общность правила и общность исполнителя — разные оси.** Правило бывает
|
||||
одно на всех и при этом требует по исполнителю на упаковку: правило
|
||||
принадлежит предметной области, исполнитель — тому, кто его поставляет.
|
||||
174. **Разрез по охвату обязан быть оплачен домом.** Разделение по глубине даёт
|
||||
два разных текста и держится само; разделение по охвату даёт два
|
||||
одинаковых, и без помеченной копии они разъезжаются — тем вернее, что
|
||||
каждый по отдельности выглядит осмысленным.
|
||||
|
||||
|
||||
## 51. av-dev-pm расколот: владение пошло по тому, что ставится порознь (2026-08-09)
|
||||
|
||||
**АЕАКЖ. Один плагин владел двумя вещами, и это мешало обеим.** `av-dev-pm` держал
|
||||
документацию проекта и учёт работ. Пока владелец был один, сцепки выглядели
|
||||
удобством: `docs.py` требовал `docs/tasks/` и звал внутрь `tasks.py`
|
||||
подпроцессом, настройки задач лежали ключом в `docs/.pm.json`, язык проектных
|
||||
текстов — внутри скилла `canon`. Каждая из трёх на расколе оказалась не
|
||||
удобством, а утверждением, что половина принадлежит другой половине. Теперь
|
||||
плагина два, `av-dev-docs` и `av-dev-tasks`, и каждый ставится сам по себе.
|
||||
|
||||
**АЕАКЗ. Самодостаточность держится копией, а не ссылкой.** Ссылка в дерево
|
||||
соседнего плагина работает ровно до того момента, когда сосед не установлен, — а
|
||||
это и есть тот случай, ради которого раскол делался. Поэтому все относительные
|
||||
ссылки, пересекшие границу, сняты: вместо них имя скилла через пространство имён
|
||||
и оговорка, что вызов может не разрешиться. То, что нужно обоим **дословно**,
|
||||
стало общим домом в `shared/`: язык проектных текстов и словарь «Сопровождение и
|
||||
эксплуатация». Второй выбран не по теме, а по числу владельцев — его делят
|
||||
роадмап, `architecture.md` и тема ревью `operations`, то есть три плагина, и ни
|
||||
один им не владеет. Три перечня «чем держат проект» уже разъезжались молча.
|
||||
|
||||
**АЕАКИ. OpenSpec отдан тому, кто им работает, а не тому, кто о нём написал.**
|
||||
Версия 7 канона объявила `openspec/` своим слотом, и разрез вышел не по владению:
|
||||
без каталога не запускается конвейер, а не канон. Заведение и форма файла уехали
|
||||
в скилл `av-dev-pipeline:openspec`, отсутствие каталога стало для `docs.py`
|
||||
неприменимостью вместо отказа. **Остаток назван, а не замолчан:** проверка формы и
|
||||
сторож версии пока остались в скрипте канона, потому что своего скрипта у
|
||||
конвейера нет ни одного, — то есть у файла сейчас два плагина, один заводит,
|
||||
другой проверяет. Это записано и в журнале версий как временное состояние.
|
||||
|
||||
### Что из этого следует
|
||||
|
||||
175. **Сцепка внутри одного владельца не видна, пока владелец один.** Она
|
||||
выглядит удобством ровно до раскола и обнаруживается не рассуждением, а
|
||||
попыткой поставить половину отдельно. Отсюда и порядок работ: сперва
|
||||
разнести, потом чинить то, что перестало сходиться.
|
||||
176. **Разрез владения идёт по тому, кто инструментом пользуется, а не по тому,
|
||||
кто о нём написал.** Канон описывал OpenSpec подробнее всех и потому казался
|
||||
его владельцем; работает по нему конвейер, и слот принадлежит конвейеру.
|
||||
|
||||
|
||||
## 52. Валидатор поехал за файлом: у конвейера появился свой скрипт (2026-08-09)
|
||||
|
||||
**АЕАКК. Названный остаток закрыт, и закрыт он ценой первого скрипта в
|
||||
конвейере.** Решение 51 отдало OpenSpec конвейеру и честно оставило хвост:
|
||||
проверка формы `config.yaml` и сторож версии остались в `docs.py`, потому что
|
||||
своего скрипта у пайплайна не было ни одного. Хвост оказался не косметическим —
|
||||
это ровно то состояние, против которого написан весь канон: **у файла два
|
||||
владельца, один заводит, другой проверяет**, и разойтись они могут молча. 252
|
||||
строки переехали в `av-dev-pipeline/skills/openspec/scripts/openspec.py`; в
|
||||
`docs.py` от темы не осталось ни константы.
|
||||
|
||||
**Переезд оплатился сразу, и не тем, чего ждали.** Прежняя проверка требовала,
|
||||
чтобы `context` называл `docs/passport.md` и `CLAUDE.md`, **безусловно** — то есть
|
||||
на проекте без канона документов требовала ссылку на несуществующий файл. Пока
|
||||
проверка жила в скрипте канона, допущение «канон есть» было незаметным: скрипт
|
||||
канона запускают там, где канон есть. В скрипте конвейера то же допущение стало
|
||||
видно на первом же прогоне. Теперь адрес требуется только к документу, который в
|
||||
проекте есть, а его отсутствие идёт строкой «не проверялось» с названной ценой.
|
||||
|
||||
### Что из этого следует
|
||||
|
||||
177. **Неявное допущение видно из другого дома, а не изнутри своего.** «Канон
|
||||
есть» было верно всюду, где код лежал, и потому не читалось как допущение
|
||||
вовсе. Переезд — самый дешёвый способ его обнаружить: не разбор, а смена
|
||||
места, из которого на код смотрят.
|
||||
|
||||
|
||||
## 53. `canon` и `docs` остаются двумя скиллами (2026-08-09)
|
||||
|
||||
**АЕАКЛ. Слияние отклонено, и довод у него не про объём.** Оба скилла лежат в
|
||||
одном плагине, и слить их казалось естественным завершением раскола. Мешает
|
||||
`description`: это не аннотация, а **триггер** — по нему загрузчик решает, звать
|
||||
ли скилл вообще, и ровно ради его сохранности заведён `frontmatter.py`. Моменты
|
||||
вызова у этих двух разные. `canon` срабатывает на «проверь документацию»,
|
||||
«переведи на канон», «пришёл в старый проект»; `docs` — на «задача сделана,
|
||||
обнови документацию», «заведи ADR», «запиши наблюдение». Одно описание покрывает
|
||||
оба хуже, чем два покрывают каждое своё, и потеря здесь не в читаемости, а в том,
|
||||
что скилл перестаёт находиться.
|
||||
|
||||
Второй довод — тот же разрез, что репозиторий подтверждал уже трижды:
|
||||
**раскладка против содержимого**, «где лежит» против «что внутри». Он по глубине,
|
||||
а такой разрез, в отличие от разреза по охвату (решение 50), даёт два разных
|
||||
текста и держится сам, без помеченных копий.
|
||||
|
||||
### Что из этого следует
|
||||
|
||||
178. **Границу между скиллами держит не тема, а момент вызова.** Два текста об
|
||||
одном предмете живут порознь законно, если зовут их в разные минуты; и
|
||||
наоборот — один предмет, разрезанный так, что оба куска нужны одновременно,
|
||||
разрезан неверно.
|
||||
|
||||
|
||||
## 54. Стык плагинов: правило получило дом, адреса остались у владельцев (2026-08-09)
|
||||
|
||||
**АЕАКМ. Вопрос пришёл с другой стороны: ревью опирается на документы проекта,
|
||||
но не должно жёстко предполагать, где файл лежит; напрашивалось оглавление
|
||||
адресов и сводка возможностей скиллов в `CLAUDE.md` проекта.** Отклонено и то и
|
||||
другое, но не потому, что проблемы нет.
|
||||
|
||||
**Оглавление адресов — второй дом раскладки.** Канон жёсток намеренно: пути
|
||||
фиксированы, проект подгоняется под них, и цена этого записана в самом каноне.
|
||||
Указатель в `CLAUDE.md` отменяет ровно эту цену — раскладка получает второе
|
||||
описание, и разойдутся они молча. Здесь молчание особенно дорогое: прогон ревью
|
||||
умеет **честно деградировать**, и протухший адрес попадает прямо в эту машинерию —
|
||||
файл не открылся, в границах покрытия появляется строка «документа в проекте
|
||||
нет», и отчёт выглядит добросовестным. Прямой путь в той же ситуации ломается
|
||||
громче.
|
||||
|
||||
**Сводка возможностей — второй дом описаний.** `description` во фронтматтере это
|
||||
триггер, по нему скилл и выбирается; переписанная руками сводка тех же описаний
|
||||
не сверяется ничем.
|
||||
|
||||
**Настоящий пробел был в другом, и он измерен.** Правило обращения к соседнему
|
||||
плагину стояло в пяти местах в пяти редакциях:
|
||||
|
||||
| Где стояло | Довод | Ветка «не разрешился» |
|
||||
| --- | --- | --- |
|
||||
| `task-pipeline` | устаревшая проектная копия | нет |
|
||||
| `task-batch` | то же | нет |
|
||||
| `review-pipeline` | вшито в пункт про удаление проектных копий | нет |
|
||||
| `openspec` | путём в чужое дерево — никогда | есть |
|
||||
| `canon` | — | есть |
|
||||
|
||||
Два разных довода, и ни в одном месте не было обоих; три места из пяти молчали о
|
||||
том, что делать при неразрешившемся вызове, — то есть о единственном, ради чего
|
||||
правило написано. Плюс невысказанный инвариант: `$CLAUDE_PLUGIN_ROOT` ведёт
|
||||
только в свой плагин, употреблён двадцать раз и нигде не оговорён — а именно он
|
||||
соблазняет дописать `/../av-dev-docs/`.
|
||||
|
||||
**Сделано:** дом `shared/plugin-boundary.md`, блок `граница-плагинов`, семь
|
||||
помеченных копий — четыре скилла конвейера и три скилла канона. В дом вошли
|
||||
полное имя, запрет пути в чужое дерево, ветка «не разрешился» с обязанностью
|
||||
доклада и признак присутствия по заведённому соседом файлу.
|
||||
|
||||
**Разрез, по которому дом наполнялся: правило общее, последствие местное.** «Нет
|
||||
`av-dev-tasks` — учёт остаётся владельцу» знает только конвейер; «нет конвейера —
|
||||
`docs.py` о каталоге `openspec/` молчит» знает только канон. Держи дом
|
||||
последствия — он знал бы наперечёт всех своих потребителей и стал бы вторым
|
||||
каноном.
|
||||
|
||||
**Адреса при этом наружу не поехали.** У них владелец есть: раскладку `docs/`
|
||||
держит канон, каталог задач — плагин задач. `shared/` заводится **только для
|
||||
фактов без владельца**; чужое с владельцем остаётся дома, а сходимость упоминаний
|
||||
в чужих деревьях проверяет машина — `scripts/addresses.py`, тем же заходом.
|
||||
|
||||
**Что выяснилось при написании чекера: судить незнакомое нельзя.** Первый прогон
|
||||
дал шесть находок, и три из них были не дрейфом, а свойством канона: `docs/**` —
|
||||
шаблон, а `docs/accessibility.md` в двух местах — пример **своей темы проекта**,
|
||||
которую канон разрешает заводить произвольно. Список тем открытый, значит
|
||||
незнакомое имя опровергнуть нечем, и проверка «есть ли такой документ у
|
||||
владельца» ловила бы законное. Переименование при этом ловится точно и по другому
|
||||
основанию: канон, убирая слот, кладёт его в карту переездов `RETIRED` — она и
|
||||
есть перечень запрещённого. Рядом одна догадка: имя, почти совпавшее с
|
||||
каноническим, читается как опечатка. Порог замерен по репозиторию — законные
|
||||
имена дают до 0.64, опечатки от 0.91, и между ними пусто.
|
||||
|
||||
Четвёртая находка оказалась настоящей: `REMAINING.md` иллюстрировал смысловой
|
||||
дубль адресом `docs/specs/recognition.md` — слотом, упразднённым в версии 1
|
||||
канона, то есть при десяти нынешних.
|
||||
Пример, который сам протух, — ровно то, ради чего чекер и писался.
|
||||
|
||||
### Что из этого следует
|
||||
|
||||
179. **Механизм честной деградации превращает протухший адрес в правдоподобный
|
||||
доклад.** Там, где отсутствие источника — законный исход с названной ценой,
|
||||
ошибка адреса неотличима от этого исхода. Значит адрес в таком месте обязан
|
||||
сверяться машиной, а не аккуратностью: единственная альтернатива —
|
||||
ломаться громко, а именно её деградация и убирает.
|
||||
180. **`shared/` — для фактов без владельца, и только.** У адресов владелец есть,
|
||||
и вынести их наружу значило бы отобрать у него его же предмет. Признак
|
||||
верного дома не «нужно нескольким», а «никому из них не принадлежит».
|
||||
181. **Общее правило и его последствия живут порознь.** Правило можно вынести в
|
||||
дом, последствие — нет: оно знает про место, а место про правило знать не
|
||||
обязано. Дом, вобравший последствия, становится реестром потребителей и
|
||||
устаревает быстрее их всех.
|
||||
182. **Проверять надо запрещённое, а не незнакомое, когда словарь открыт.**
|
||||
Открытый список делает «нет такого имени» неопровержимым, и проверка на
|
||||
принадлежность перечню начинает ловить законное. Ловится ровно то, что
|
||||
владелец объявил упразднённым: карта переездов — не побочный артефакт
|
||||
миграции, а перечень запрещённого, и стоит она ровно там, где нужна.
|
||||
183. **Замер порога записывается рядом с порогом.** Число, выбранное на глаз,
|
||||
через месяц неотличимо от подогнанного под один случай. Обе стороны разрыва
|
||||
названы (0.64 и 0.91) — и видно не только, что порог верен, но и насколько
|
||||
он не на грани.
|
||||
|
||||
## 55. `task-pipeline` стал `resolve`: два плановых стопа вместо полной автономии (2026-08-09)
|
||||
|
||||
**АЕАКН. Автоматическое решение задач агентом признано утопией — «работает, но
|
||||
работает плохо», — и хуже того, автор перестал ориентироваться в собственном
|
||||
процессе.** Отсюда разворот: задачи решаются по одной, а в цикл возвращается
|
||||
человек. `task-batch` удалён целиком; `task-pipeline` переписан в `resolve`.
|
||||
|
||||
**Прежняя доктрина звучала «умолчание — делать, а не спрашивать», и она не
|
||||
отменена, а ограничена.** Полностью автономный прогон плох не тем, что ошибается,
|
||||
а тем, что ошибку видно на готовом коде: развилка, стоившая бы абзаца до
|
||||
`propose`, стоит переписывания после `apply`. Постоянное же согласование
|
||||
возвращает ту цену, ради ухода от которой пайплайн и писался. Разрез поэтому по
|
||||
**месту**, а не по важности решения: развилка, найденная до ближайшего чекпоинта,
|
||||
копится в него; найденная после последнего — по-прежнему уходит вопросом в запись,
|
||||
и задача доводится в объявленных границах.
|
||||
|
||||
**Чекпоинтов два, и второй обязателен всегда.**
|
||||
|
||||
- **«варианты»** — у исследовательской задачи, до первого требования. Признак
|
||||
ветки не объём работы, а **отсутствие одного очевидного способа решения**:
|
||||
обсуждать варианты после `propose` поздно, предложение уже воплотило один из
|
||||
них, и разговор пойдёт не о выборе, а о переделке. Форма ограничена сверху —
|
||||
2–4 варианта: больше четырёх человек не сравнивает, а признаёт неспособность
|
||||
сравнить и просит рекомендацию.
|
||||
- **«объяснение»** — у всякой задачи, **после** ревью дизайна. Порядок обоснован:
|
||||
человек читает то, что уже просеяла машина, и не тратит внимание на выловимое
|
||||
`review-specs`. Внимание здесь самый дорогой ресурс процесса.
|
||||
|
||||
**Объяснение не стало новым артефактом, и это главная правка первоначального
|
||||
замысла.** Задумывалось отдельным разделом в `design.md`; при разборе оказалось,
|
||||
что оно там было бы **третьим домом** одного и того же: в `proposal.md` уже есть
|
||||
`## Why` («в чём проблема»), в `design.md` — рассмотренные варианты. Поэтому
|
||||
объяснение **собирается из двух существующих артефактов**, а требование к их
|
||||
форме уехало в `openspec/config.yaml` — `rules.proposal` и `rules.design`. Это
|
||||
единственное место, применяющееся **в момент написания**, а не после.
|
||||
Побочная выгода: `design.md` с названными причинами отказа — половина будущего
|
||||
ADR, а промоут ADR читает именно архивный `design.md`.
|
||||
|
||||
**Закрыт вопрос, висевший в плане открытым: что делает автоматический участок,
|
||||
когда ревью кода спорит с одобренным дизайном.** Признак проверяемый —
|
||||
**меняются ли дельта-спеки**. Не меняются: находка внутри дизайна, дожимается
|
||||
сама. Меняются: решение стало другим, а одобрено было прежнее — разметка
|
||||
пересчитывается (правило уже было) и **чекпоинт повторяется**. Чекпоинт, который
|
||||
можно обойти находкой ревью, не значит ничего, и хуже того — человек уверен, что
|
||||
одобрил именно то, что уехало в коммит.
|
||||
|
||||
**Удаление `task-batch` обошлось дороже своего каталога.** На нём держались:
|
||||
третий режим `review-specs` (стык после слияния) вместе с исключением «живого
|
||||
change нет — берём источником актуальные спеки»; единственное исключение из
|
||||
правила `review-triage` «плана нет — не запускаюсь»; и обоснование имени основной
|
||||
ветки в каноне — «в неё вливает батч». Первые два — послабления, существовавшие
|
||||
только ради батча, и с ним они исчезли, сделав оба правила строже.
|
||||
|
||||
### Что из этого следует
|
||||
|
||||
184. **Автономность ограничивается местом, а не важностью решения.** «Спрашивать
|
||||
о важном» неисполнимо: важность оценивает тот же, кто хочет закончить.
|
||||
«Копить до ближайшего планового стопа» проверяемо и не требует суждения.
|
||||
185. **Чекпоинт ставится после машинной проверки, а не до неё.** Внимание
|
||||
человека тратится только на то, чего машина не ловит; порядок наоборот
|
||||
сжигает его на выловимом и обесценивает саму остановку.
|
||||
186. **Объяснение для человека не заводит своего артефакта.** Если оно
|
||||
собирается из уже существующих, оно не может с ними разойтись; отдельный
|
||||
текст «то же, но понятнее» — третий дом, и расходится он молча.
|
||||
187. **Послабление, введённое ради одного потребителя, уходит вместе с ним.**
|
||||
Исключение переживает своего заказчика и выглядит общим правилом; удаляя
|
||||
потребителя, ищи его исключения — они и есть настоящий хвост.
|
||||
|
||||
## 56. `av-dev-pipeline` → `av-dev-code`, `review-pipeline` → `review` (2026-08-09)
|
||||
|
||||
**АЕАКО. Имя описывало устройство, а не предмет.** «Пайплайн» говорит, что внутри
|
||||
конвейер, — а плагин занят кодом по задачам, и после появления чекпоинтов он уже
|
||||
не конвейер в чистом виде: между остановками автоматика, на остановках разговор.
|
||||
|
||||
**Набор имён стал параллельным, и это довод сам по себе:** `docs` / `tasks` /
|
||||
`code` / `git` — каждое называет **материал**, которым плагин занят. Прежнее имя
|
||||
выбивалось: три существительных и одна метафора устройства. По той же причине
|
||||
отвергнут `av-dev-solve` — глагол в ряду существительных, плюс заикание в главном
|
||||
вызове (`solve:resolve`), — и `av-dev-work`: «работы» в этом репозитории уже
|
||||
значат конкретное (цели и задачи роадмапа, секция «Сопровождение»), и имя начало
|
||||
бы спорить со словарём.
|
||||
|
||||
Заодно `review-pipeline` стал `review`: слово «пайплайн» ушло из плагина целиком,
|
||||
а не наполовину, и скиллы выровнялись — `resolve` / `review` / `openspec`.
|
||||
|
||||
**Журнал версий канона переписан вместе со всеми, и это не нарушение правила «не
|
||||
переписываем задним числом».** Разрез проходит не по типу файла, а по типу
|
||||
высказывания. Наблюдение и причина — неприкосновенны: их правка есть
|
||||
фальсификация. **Предписание и адрес обязаны оставаться исполнимыми**: запись
|
||||
версии 10 велит «проверить, что плагин `av-dev-pipeline` установлен», и проект,
|
||||
дошедший до неё, выполнит невыполнимое. `DECISIONS.md` при этом не тронут — в нём
|
||||
нет предписаний проекту, только записи о принятых решениях; там прежнее имя
|
||||
верно, потому что описывает состояние на дату записи.
|
||||
|
||||
### Что из этого следует
|
||||
|
||||
188. **Имя плагина называет материал, а не устройство.** Устройство меняется —
|
||||
конвейер обзавёлся остановками, — а материал остаётся. Имя по устройству
|
||||
протухает первым и при этом выглядит осмысленным.
|
||||
189. **Журнал не переписывается в наблюдениях и обязан оставаться исполнимым в
|
||||
предписаниях.** Правило «не задним числом» защищает от подделки фактов, а не
|
||||
от починки инструкций: инструкция, ссылающаяся на несуществующее, — не
|
||||
свидетельство эпохи, а поломка с отложенным сроком.
|
||||
|
||||
## 57. Спринты отменены: приоритет стал порядком строк, `session` стал `groom` (2026-08-09)
|
||||
|
||||
**АЕАКП. Спринт отвечал на вопрос «что делать дальше» замороженным набором, а
|
||||
между наборами на этот вопрос не отвечал никто.** Процесс идёт задача за задачей,
|
||||
и набор перестал что-либо удерживать: он не синхронизировал (некого), не
|
||||
ограничивал по времени (тайм-бокс не брали) и не защищал от врывания (врывалось
|
||||
ровно два класса, оба назывались правилом). Осталась цена — обязанность собрать,
|
||||
показать, заморозить и распустить.
|
||||
|
||||
**Приоритет вернулся, и вернулся туда, где ему место.** Прежнее правило «порядка
|
||||
нет, есть цель» было обосновано **набором спринта**, и с ним потеряло опору.
|
||||
Приоритет — свойство очереди, а не задачи, поэтому его дом **индекс**: то же
|
||||
исключение из правила «файл — источник истины», что уже было у «в каком индексе
|
||||
лежит запись». Числом в файле он быть не мог — два соседних файла смогли бы
|
||||
утверждать одно место, а строка индекса противоречить обоим.
|
||||
|
||||
**Гейт готовности стоял на `sprint take` и чуть не исчез вместе с ним.** Это было
|
||||
единственное место, где запись судили целиком: тип, цель у `feature`, пустой
|
||||
раздел вопросов, схема типа. Без спринта момента не осталось бы вовсе, а узнают
|
||||
о недописанной задаче на приёмке, когда сверять уже не с чем. Момент назвали
|
||||
заново — команда `tasks.py ready <слаг>`, и зовёт её тот, кто берёт задачу.
|
||||
Отказ там **код 1, а не 2**: запись не дописана — это рабочая ситуация, а не
|
||||
ошибка употребления.
|
||||
|
||||
**`session` стал `groom`, и предмет сузился до двух вопросов** — что сейчас
|
||||
самое важное и что перестало быть важным. Из четырёх шагов прежней сессии выжили
|
||||
два (вопросы, переоценка порциями), один заменился (расстановка очереди вместо
|
||||
набора спринта), два выпали:
|
||||
|
||||
- **приёмка закрытых задач** — грумингу не по предмету. Ритуала у неё больше нет,
|
||||
остаётся `reopen` по требованию. Цена названа прямо: приёмка происходит только
|
||||
тогда, когда что-то уже бросилось в глаза;
|
||||
- **разбор процесса** — его якорем был прошедший спринт. Вместе с ним из скилла
|
||||
ушёл прямой вызов агентов `doc-consistency` и `doc-code-drift`, и это **не
|
||||
потеря, а починка**: агенты принадлежат `av-dev-docs`, и груминг звал их мимо
|
||||
правила обращения к соседу, без ветки «плагина нет». Груминг теперь только
|
||||
**называет повод** сверить канон, а когда их звать — решает их владелец.
|
||||
|
||||
**Побочно найдено:** `canon.md` — дом определения канона — объявлял себя версией
|
||||
7, когда скрипт шёл на 11. Пять версий дом врал о себе, и не заметил никто:
|
||||
машина сверяет версию проекта с константой скрипта, а прозу в заголовке не
|
||||
читает.
|
||||
|
||||
### Что из этого следует
|
||||
|
||||
190. **Правило, обоснованное механикой, умирает вместе с ней — и надо проверять,
|
||||
что вопрос умер тоже.** «Порядка нет» держалось на наборе спринта; набор
|
||||
ушёл, а вопрос «что делать дальше» остался и повис без ответа. Снимая
|
||||
механику, ищи не только то, что на ней стояло, но и то, на что она отвечала.
|
||||
191. **Гейт живёт в моменте, а не в команде.** Проверка готовности была свойством
|
||||
`sprint take` — и была бы потеряна как деталь удаляемой команды. Момент
|
||||
«запись впервые судят целиком» существует независимо от того, чем он
|
||||
назван, и переезжает вместе с процессом.
|
||||
192. **Версия в прозе, которую не читает машина, протухает молча.** Дом канона
|
||||
назвал себя версией 7 при текущей 11: сверка шла по константе скрипта, а
|
||||
заголовок документа не сверял никто.
|
||||
|
||||
## 58. Судьи документов получили свой скилл — `healthcheck` (2026-08-09)
|
||||
|
||||
**АЕАКР. Момент вызова был свойством чужого ритуала и исчез вместе с ним.**
|
||||
`doc-consistency` и `doc-code-drift` звались шагом сессии между спринтами. Сессия
|
||||
стала грумингом, груминг судит задачи, а не документы, и звать чужих агентов он
|
||||
не вправе — они живут в `av-dev-docs`. На живом проекте их не звал бы **никто**,
|
||||
кроме разовых `adopt` и `upgrade`.
|
||||
|
||||
Чинить это возвратом вызова в груминг было нельзя: это ровно то нарушение
|
||||
границы, которое там и обнаружилось (вызов агента чужого плагина по имени, без
|
||||
ветки «плагина нет»). Момент нужно было назвать **у владельца** — и оказалось,
|
||||
что владельца-то у них и нет: `canon` их звал, но владел раскладкой, а не
|
||||
суждением.
|
||||
|
||||
**Скилл `av-dev-docs:healthcheck`.** Предмет — то, чего машина не видит:
|
||||
разошлись ли документы между собой и с кодом. Разрез с `canon check` проверяемый:
|
||||
**машина сверяет форму, healthcheck — утверждения.** «Раздел есть» проверит
|
||||
скрипт; «написано, что зависимость одна, а в манифесте их три» — суждение.
|
||||
|
||||
**Почему скилл, а не просто описание агентов.** Триггер у агента и так есть — его
|
||||
`description`. Но двоим нужна **оркестровка**: позвать обоих на весь канон разом,
|
||||
передать `doc-code-drift` раздел запретов, разобрать урожай порциями, назвать
|
||||
границы покрытия и то, кого именно позвал. Этого агент о себе не знает.
|
||||
|
||||
**`doc-wording` внутрь не взят, и это разрез, а не забывчивость.** Ему
|
||||
оркестровка не нужна: он один и работает по названному списку документов. И ритм
|
||||
другой — он нужен там, где текст только что писали, а не там, где он год лежал.
|
||||
Скилл, собравший всех троих «потому что все про документы», склеил бы разные
|
||||
вопросы под одним вызовом.
|
||||
|
||||
### Что из этого следует
|
||||
|
||||
193. **Момент вызова — такая же собственность, как сам инструмент.** Агент,
|
||||
чей момент назначен чужим ритуалом, теряет его вместе с ритуалом и
|
||||
замолкает беззвучно: он исправен, его просто никто не зовёт.
|
||||
194. **Оркестровка — вот что отличает скилл от агента.** Одному исполнителю с
|
||||
ясным входом скилл не нужен, его находит описание. Скилл заводят там, где
|
||||
надо решить, кого звать, что передать, в каком объёме и что делать с
|
||||
результатом.
|
||||
|
||||
## 59. Аудит четырьмя сабагентами: описания отстают от механики молча (2026-08-09)
|
||||
|
||||
Реорганизация была объявлена законченной «на бумаге», и я запустил по аудитору на
|
||||
плагин — консистентность, самостоятельность, интегрируемость. Гейт при этом был
|
||||
зелёным и остался честен: он проверяет ровно то, что умеет.
|
||||
|
||||
Нашлось около полусотни расхождений, и они **одного рода**. Каждый раз я правил
|
||||
механику — вырезал спринт из скрипта, переименовал скиллы, перенёс судей — и
|
||||
каждый раз не правил то, что механику **описывает вовне**: скелеты документов,
|
||||
докстринги скрипта, уставы агентов, манифесты плагинов, README.
|
||||
|
||||
Самое дорогое: **скелет `CLAUDE.md` уносил слоты спринта в каждый новый проект**
|
||||
через две недели после отмены спринтов. Скелет не описывает, а порождает: его
|
||||
отставание не читается, оно исполняется.
|
||||
|
||||
**Механизм приоритета не запускался ни разу.** `--section` у `move` был
|
||||
обязательным, а все три места, где груминг предписывает расстановку, дают команду
|
||||
без него — usage error. Скилл написан, прогнан не был, и разницы между рабочим и
|
||||
бумажным процессом не видно, пока его не запустят.
|
||||
|
||||
**Правило границы я же и нарушал.** Восемь дословных копий «путь в дерево чужого
|
||||
плагина не пишется никогда» — и пять мест, где путь написан, одно из них строкой
|
||||
выше собственного «пути туда конвейер не выносит».
|
||||
|
||||
Отдельно: **у описания плагина было два дома**, и три из четырёх разошлись. Класс
|
||||
закрыт не дисциплиной, а машиной — `frontmatter.py` теперь сверяет `plugin.json` с
|
||||
`marketplace.json`, а гейт разбужен на `*.json`.
|
||||
|
||||
Правки разобраны четырьмя пропусками по одному сабагенту на пропуск, с проверкой
|
||||
результата каждого: скелеты и канон, исполнимость учёта задач, границы и стыки,
|
||||
словарь и манифесты. Скриптовые правки проверены поведением на фикстурах, включая
|
||||
настоящий git-репозиторий для `reopen`.
|
||||
|
||||
**Чего аудит не даёт.** Это было чтение. Ни один скилл по-прежнему не исполнялся
|
||||
на живом проекте, и находки вроде «чекпоинт вырождается в ритуал» такой проверкой
|
||||
не берутся по построению.
|
||||
|
||||
### Что из этого следует
|
||||
|
||||
195. **Механика проверяется прогоном, описание — только чтением.** Поэтому после
|
||||
каждой правки механики отстают именно описания, и отстают молча. Меняя
|
||||
механику, ищи её отражения поимённо: скелеты, докстринги, уставы агентов,
|
||||
манифесты, README.
|
||||
196. **Скелет дороже документа: он не описывает, а порождает.** Отставший
|
||||
документ врёт одному читателю; отставший скелет уезжает в каждый новый
|
||||
проект и становится там обязательным.
|
||||
197. **Копия правила не заставляет его исполнять.** Правило исполняется там, где
|
||||
его проверяет машина или чужой глаз; восемь копий на видном месте не
|
||||
помешали автору нарушить его пятью строками.
|
||||
198. **Бумажный процесс неотличим от рабочего, пока его не запустили.** Команда,
|
||||
которую никто не набрал, может не существовать вовсе — и именно так и было.
|
||||
199. **Два дома у факта расходятся не когда-нибудь, а сразу.** Из четырёх пар
|
||||
описаний плагина совпала одна — та, которую с момента заведения не правили.
|
||||
|
||||
@@ -9,26 +9,47 @@
|
||||
|
||||
## Плагины
|
||||
|
||||
- **av-dev-pm** — управление продуктом. Владеет всем `docs/`.
|
||||
- **av-dev-docs** — документация проекта. Владеет `docs/` и `CLAUDE.md`.
|
||||
- `init` — новый проект: интервью по свободному описанию замысла → первичная
|
||||
документация;
|
||||
- `canon` — привести проект к канону документов: `check` / `adopt` /
|
||||
`upgrade`, плюс скрипт `docs.py`. Там же живёт язык проектных текстов —
|
||||
информационный стиль, англицизмы, жаргон. Смысловую часть, которой скрипт
|
||||
не видит, судят два агента: `doc-consistency` (документы между собой и с
|
||||
openspec) и `doc-code-drift` (документы против кода);
|
||||
`upgrade`, плюс скрипт `docs.py`. Там же лежит копия языка проектных
|
||||
текстов — информационный стиль, англицизмы, жаргон; дом у него общий,
|
||||
`shared/language.md`;
|
||||
- `healthcheck` — здоровье документации **судом, а не машиной**: не разошлись
|
||||
ли документы между собой и с кодом. Зовёт двух агентов на весь канон разом —
|
||||
`doc-consistency` (документы между собой и с openspec) и `doc-code-drift`
|
||||
(факты против кода) — и разбирает урожай порциями. Дорого, поэтому не на
|
||||
каждой задаче. Язык документов вычитывает отдельный агент `doc-wording`, и
|
||||
зовут его не отсюда, а те, кто только что писал текст: `docs`, `init` и
|
||||
`canon`;
|
||||
- `docs` — содержимое канона по ходу разработки: ADR из архивного
|
||||
`design.md`, промоут конвенций, запись в разведку и журнал ревью, чистка
|
||||
архитектуры;
|
||||
архитектуры.
|
||||
- **av-dev-tasks** — учёт работ. Владеет каталогом задач.
|
||||
- `tasks` — задачи и цели каталогом markdown-файлов, у каждой записи тип
|
||||
(`goal`, `feature`, `fix`, `chore`, `research`), и тип задаёт её схему;
|
||||
вычитывают их два отдельных прохода: `task-form` (форма записи) и
|
||||
`doc-wording` (язык);
|
||||
- `session` — ритуал между спринтами и ведение спринта.
|
||||
- **av-dev-pipeline** — исполнение. **Требует OpenSpec.**
|
||||
- `task-pipeline` — задача через полный цикл SDD, от постановки до коммита;
|
||||
- `task-batch` — несколько задач разом, каждая в своём worktree;
|
||||
- `review-pipeline` — конвейер ревью **по темам**: документ проекта либо
|
||||
`task-wording` (язык записей);
|
||||
- `groom` — груминг беклога: что сейчас самое важное и что перестало быть
|
||||
важным. Ответ записывается **порядком строк** — приоритет это свойство
|
||||
очереди, и живёт он в индексе. Интерактивный: разбирает вопросы пачкой,
|
||||
переоценивает порциями по 5–8, расставляет верх очереди с доводом на
|
||||
каждое движение.
|
||||
- **av-dev-code** — код по задачам: решение одной задачи и его проверка.
|
||||
Владеет `openspec/`. **Требует OpenSpec и сам его заводит.**
|
||||
- `openspec` — завести, настроить и **проверить** `openspec/` в проекте:
|
||||
`openspec init`, замена примера в `config.yaml` настройкой канонической
|
||||
формы, скрипт `openspec.py` (форма файла + сверка слепка с живой версией
|
||||
инструмента). Каталог принадлежит конвейеру, а не канону: без конвейера он
|
||||
проекту не нужен, и `docs.py` о нём молчит;
|
||||
- `resolve` — одна задача от постановки до закрытия. Обычная идёт циклом SDD
|
||||
с **чекпоинтом после ревью дизайна**: объяснение человеческим языком, повод
|
||||
скорректировать ход решения. Исследовательская начинается с `opsx:explore` и
|
||||
**чекпоинта вариантов** — способы решить, цена каждого, рекомендация; выбор
|
||||
оседает по адресу, который назвала сама задача. Между чекпоинтами — без
|
||||
согласований;
|
||||
- `review` — конвейер ревью **по темам**: документ проекта либо
|
||||
заводит тему проверки, либо питает чужую тему источником, либо процессный и в
|
||||
ревью не читается вовсе. Разметка идёт **один раз на задачу**, сразу после
|
||||
`propose`: агент `review-scope` меряет изменение по двум осям — размер и
|
||||
@@ -42,23 +63,37 @@
|
||||
Соглашение об именах: имя **плагина** длинное с префиксом `av-dev-`, имена
|
||||
**скилов** внутри — короткие. Вызов выходит вида `/av-dev-<плагин>:<скилл>`.
|
||||
|
||||
Кто кого зовёт (стрелка — вызов через пространство имён, не импорт):
|
||||
Кто кого зовёт. Сплошная стрелка — вызов скилла через пространство имён, не
|
||||
импорт; пунктир — совет позвать, а не вызов. Агенты-проходы в графе не показаны:
|
||||
их зовут скиллы, названные выше.
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph pipe["av-dev-pipeline — исполнение, требует OpenSpec"]
|
||||
subgraph pipe["av-dev-code — исполнение, требует OpenSpec"]
|
||||
direction LR
|
||||
batch["task-batch"] --> tp["task-pipeline"]
|
||||
tp --> rp["review-pipeline<br/>10 агентов-проходов"]
|
||||
batch --> rp
|
||||
tp["resolve<br/>2 чекпоинта человеку"] --> rp["review<br/>10 агентов-проходов"]
|
||||
osp["openspec<br/>заводит и проверяет openspec/"]
|
||||
end
|
||||
subgraph pm["av-dev-pm — управление продуктом, владеет docs/"]
|
||||
subgraph docsp["av-dev-docs — документация, владеет docs/"]
|
||||
direction LR
|
||||
init["init"] --> tasks["tasks"]
|
||||
canon["canon"] --> tasks
|
||||
session["session"] --> tasks
|
||||
init["init"]
|
||||
canon["canon"]
|
||||
docs["docs"]
|
||||
hc["healthcheck"]
|
||||
end
|
||||
subgraph tasksp["av-dev-tasks — учёт работ"]
|
||||
direction LR
|
||||
groom["groom"] --> tasks["tasks"]
|
||||
end
|
||||
init --> tasks
|
||||
init --> osp
|
||||
canon --> tasks
|
||||
canon --> osp
|
||||
canon --> hc
|
||||
hc --> tasks
|
||||
docs --> rp
|
||||
rp --> tasks
|
||||
groom -.-> hc
|
||||
opsx["opsx:* — внешний плагин:<br/>explore, propose, apply, archive"]
|
||||
git["av-dev-git: commit"]
|
||||
|
||||
@@ -68,9 +103,18 @@ flowchart TB
|
||||
tp --> tasks
|
||||
```
|
||||
|
||||
Зависимость **односторонняя: `av-dev-pipeline` знает про `av-dev-pm`, обратно —
|
||||
нет.** Управление продуктом работает в проекте без конвейера; конвейер без
|
||||
канона деградирует поразрядно и говорит об этом строкой.
|
||||
Зависимости **взаимные, но каждая мягкая**. `av-dev-code` зовёт обоих соседей;
|
||||
обратные вызовы тоже есть — `av-dev-docs:init` и `av-dev-docs:canon` заводят
|
||||
OpenSpec скиллом `av-dev-code:openspec`, `av-dev-docs:docs` берёт у
|
||||
`av-dev-code:review` форму записи журнала дефектов и процедуру промоута,
|
||||
`av-dev-docs:canon` и `av-dev-docs:healthcheck` зовут `av-dev-tasks:tasks`.
|
||||
**Мягкая** значит, что у любого вызова есть ветка «не разрешился»: соседа в
|
||||
проекте нет — вызывающий называет строкой, чего теперь не делает никто, и работу
|
||||
не останавливает. Как именно зовут соседа и что делают, когда вызов не
|
||||
разрешился, — `shared/plugin-boundary.md`: правило нужно большинству скиллов, и
|
||||
ни один плагин им не владеет. То, что нужно нескольким дословно — граница
|
||||
плагинов, язык проектных текстов, словарь сопровождения, — живёт домом в
|
||||
`shared/` и уезжает в каждый плагин помеченной копией.
|
||||
|
||||
## Канон документов проекта
|
||||
|
||||
@@ -79,7 +123,7 @@ flowchart TB
|
||||
`docs/` (паспорт, архитектура, база, безопасность, конвенции, разведка, ADR,
|
||||
ревью, задачи) и `openspec/` — **раскладка целиком, роли документов и правило
|
||||
единственного дома живут одним домом**:
|
||||
[canon.md](av-dev-pm/skills/canon/references/canon.md). Здесь она не
|
||||
[canon.md](av-dev-docs/skills/canon/references/canon.md). Здесь она не
|
||||
пересказывается: копия перечня путей уже расходилась с домом, и как раз в
|
||||
обязательных — в ней не хватало путей, чьё отсутствие `docs.py check` считает
|
||||
нарушением.
|
||||
@@ -95,11 +139,11 @@ flowchart TB
|
||||
|
||||
Отдельного файла-брифа при этом нет — проходы читают документы напрямую; карта
|
||||
«тема → её дом → что оттуда берётся» —
|
||||
[project-facts.md](av-dev-pipeline/skills/review-pipeline/references/project-facts.md).
|
||||
[project-facts.md](av-dev-code/skills/review/references/project-facts.md).
|
||||
|
||||
Прийти в старый проект и перевести его на канон — `/av-dev-pm:canon`. Канон
|
||||
Прийти в старый проект и перевести его на канон — `/av-dev-docs:canon`. Канон
|
||||
версионируется, и проекты повышаются по [журналу
|
||||
версий](av-dev-pm/skills/canon/references/changelog.md).
|
||||
версий](av-dev-docs/skills/canon/references/changelog.md).
|
||||
|
||||
## Подключение
|
||||
|
||||
@@ -114,8 +158,9 @@ cd /path/to/project
|
||||
claude plugin marketplace add https://git.vakhrushev.me/av/dev-skills.git --scope project
|
||||
|
||||
# плагины: scope обязателен, умолчание у команды — user, а нам нужен project
|
||||
claude plugin install av-dev-pm@av-dev-skills --scope project
|
||||
claude plugin install av-dev-pipeline@av-dev-skills --scope project
|
||||
claude plugin install av-dev-docs@av-dev-skills --scope project
|
||||
claude plugin install av-dev-tasks@av-dev-skills --scope project
|
||||
claude plugin install av-dev-code@av-dev-skills --scope project
|
||||
claude plugin install av-dev-git@av-dev-skills --scope project
|
||||
```
|
||||
|
||||
@@ -131,15 +176,16 @@ claude plugin install av-dev-git@av-dev-skills --scope project
|
||||
}
|
||||
},
|
||||
"enabledPlugins": {
|
||||
"av-dev-pm@av-dev-skills": true,
|
||||
"av-dev-pipeline@av-dev-skills": true,
|
||||
"av-dev-docs@av-dev-skills": true,
|
||||
"av-dev-tasks@av-dev-skills": true,
|
||||
"av-dev-code@av-dev-skills": true,
|
||||
"av-dev-git@av-dev-skills": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**При установке в проект, где лежали проектные копии** скиллов и агентов
|
||||
(`.claude/skills/` — голые `task-pipeline`, `review-pipeline`, `task-batch`
|
||||
(`.claude/skills/` — голые `task-pipeline`, `review-pipeline`
|
||||
и с префиксом проекта `<проект>-task-pipeline`,
|
||||
`.claude/agents/<проект>-review-*.md`) — снеси их. Две копии одного скилла
|
||||
расходятся, и побеждает та, что короче названа.
|
||||
@@ -161,8 +207,9 @@ claude plugin marketplace update av-dev-skills
|
||||
|
||||
# 2. снимки плагинов — из каталога проекта, где они установлены
|
||||
cd /path/to/project
|
||||
claude plugin update av-dev-pm@av-dev-skills --scope project
|
||||
claude plugin update av-dev-pipeline@av-dev-skills --scope project
|
||||
claude plugin update av-dev-docs@av-dev-skills --scope project
|
||||
claude plugin update av-dev-tasks@av-dev-skills --scope project
|
||||
claude plugin update av-dev-code@av-dev-skills --scope project
|
||||
claude plugin update av-dev-git@av-dev-skills --scope project
|
||||
```
|
||||
|
||||
@@ -234,9 +281,10 @@ claude plugin uninstall <плагин>@av-dev-skills --scope project
|
||||
<plugin>/.claude-plugin/plugin.json манифест плагина
|
||||
<plugin>/skills/<skill>/SKILL.md скилы (авто-обнаружение)
|
||||
<plugin>/skills/<skill>/references/ что читается по ссылке из скилла
|
||||
<plugin>/skills/<skill>/scripts/ tasks.py, docs.py
|
||||
<plugin>/skills/<skill>/scripts/ tasks.py, docs.py, openspec.py
|
||||
<plugin>/agents/ charter'ы сабагентов
|
||||
scripts/ проверки репозитория: копии, диаграммы, фронтматтеры
|
||||
shared/ дома правил, общих для нескольких плагинов
|
||||
scripts/ проверки репозитория и пересборка копий
|
||||
pyproject.toml линтеры скриптов, только для этого репозитория
|
||||
lefthook.yml гейт коммита: проверки документов
|
||||
```
|
||||
@@ -259,17 +307,17 @@ uv run pyrefly check # типы
|
||||
линтеров, и любой сторонний импорт у него не разрешается. Список запретов —
|
||||
не перечень мира, настоящий страж второй.
|
||||
|
||||
## Проверка фронтматтеров
|
||||
## Проверка фронтматтеров и описаний плагинов
|
||||
|
||||
Фронтматтер читает не человек, а загрузчик: по `name` он разрешает вызов, по
|
||||
`description` решает, звать ли скилл вообще. **Ошибка здесь не выглядит
|
||||
ошибкой** — тем же способом, что и в диаграммах.
|
||||
|
||||
```
|
||||
uv run python scripts/frontmatter.py # 0 в порядке, 1 расхождение, 3 не тот каталог
|
||||
python3 scripts/frontmatter.py # 0 в порядке, 1 расхождение, 3 не тот каталог
|
||||
```
|
||||
|
||||
Ловится три класса:
|
||||
Ловится четыре класса:
|
||||
|
||||
- **двоеточие с пробелом в описании без кавычек.** Для YAML `: ` внутри
|
||||
простого скаляра начинает вложенное отображение, так что «конвейер ревью: гейт,
|
||||
@@ -281,10 +329,16 @@ uv run python scripts/frontmatter.py # 0 в порядке, 1 расхожд
|
||||
а не «имя не то»;
|
||||
- **цвет charter'а, не отвечающий его модели.** Цвет кодирует модель, а не роль
|
||||
прохода — раскладка живёт в
|
||||
[review-pipeline/SKILL.md](av-dev-pipeline/skills/review-pipeline/SKILL.md),
|
||||
[review/SKILL.md](av-dev-code/skills/review/SKILL.md),
|
||||
разделе «Модель по проходу», здесь только её механизация. Держаться вниманием
|
||||
правило не может: цвет ставится один раз при заведении charter'а, а модель
|
||||
потом меняется калибровкой.
|
||||
потом меняется калибровкой;
|
||||
- **описание плагина, разошедшееся между манифестами.** У описания два дома:
|
||||
`<плагин>/.claude-plugin/plugin.json` показывает его установленному плагину,
|
||||
корневой `.claude-plugin/marketplace.json` — тому, кто выбирает, ставить ли.
|
||||
Правят обычно один, и разойтись они успели уже трижды из четырёх. `copies.py`
|
||||
этот класс не берёт: он смотрит markdown, а манифест — json. Отсюда и `*.json`
|
||||
в глобе задачи гейта.
|
||||
|
||||
## Проверка копий правил
|
||||
|
||||
@@ -293,7 +347,7 @@ uv run python scripts/frontmatter.py # 0 в порядке, 1 расхожд
|
||||
говорить. Значит копия допустима, но **дословная и помеченная**:
|
||||
|
||||
```
|
||||
uv run python scripts/copies.py # 0 сошлось, 1 расхождение, 2 разметка, 3 не тот каталог
|
||||
python3 scripts/copies.py # 0 сошлось, 1 расхождение, 2 разметка, 3 не тот каталог
|
||||
```
|
||||
|
||||
Разметка — HTML-комментарии, невидимые в отрендеренном markdown:
|
||||
@@ -311,10 +365,84 @@ uv run python scripts/copies.py # 0 сошлось, 1 расхождение
|
||||
сверку не входят. Маркер, уехавший в проект вместе со скелетом, там полезен: он
|
||||
говорит, что у текста есть дом и правится он там.
|
||||
|
||||
**Дом правила, общего для нескольких плагинов, лежит в `shared/` и ни одному из
|
||||
них не принадлежит.** Так живёт язык проектных текстов: он одинаково нужен
|
||||
документам канона и задачам, и хранить его внутри одного плагина значило бы
|
||||
отдать общее правило во владение половине. Так же живёт граница плагинов —
|
||||
правило обращения к соседу. Плагин везёт копию и потому остаётся
|
||||
самодостаточным — `shared/` нужен этому репозиторию, а не установленному
|
||||
плагину.
|
||||
|
||||
**Дом ставится в `shared/` только тогда, когда владельца нет.** У адресов
|
||||
владелец есть: раскладку `docs/` держит канон, каталог задач — плагин задач, и
|
||||
переносить их наружу значило бы отобрать у владельца его же предмет. Общее без
|
||||
владельца едет копией из `shared/`; чужое с владельцем остаётся дома, а
|
||||
потребитель на него ссылается.
|
||||
|
||||
Скрипт ловит четыре вещи: копия разошлась с домом (с диффом), копия указывает не
|
||||
на тот файл, дом остался без копий, разметка сломана. Чего он **не** ловит —
|
||||
копию, которую забыли пометить: помечать — по-прежнему решение человека.
|
||||
|
||||
### Пересборка — `scripts/resync.py`
|
||||
|
||||
```
|
||||
python3 scripts/resync.py # переписать тела всех разошедшихся копий из домов
|
||||
# 0 готово, 2 разметка сломана, 3 не тот каталог
|
||||
```
|
||||
|
||||
Правка дома касается стольких файлов, сколько у него копий, и последний из них
|
||||
забывают — это и есть причина, по которой копии расходятся. Пересборка делает то
|
||||
же машиной и потому дословна по построению.
|
||||
|
||||
**В гейт коммита скрипт не ставится, и это решение.** Автоматическая пересборка
|
||||
протащила бы правку дома во все копии мимо глаз автора, а правка дома, чья копия
|
||||
уезжает в репозиторий проекта, обязана ещё и попасть в журнал версий канона —
|
||||
этого машина не напишет. Гейт поэтому только **называет** расхождение; согласие с
|
||||
ним остаётся действием человека.
|
||||
|
||||
Разметку разбирает не он сам: `copies.py` импортируется целиком. Второй
|
||||
разборщик той же разметки разошёлся бы с первым молча — ровно тот класс дефекта,
|
||||
против которого механика копий и заведена.
|
||||
|
||||
**Ограда блока кода принадлежит месту, а не дому.** Одно и то же тело живёт в
|
||||
доме внутри ```` ``` ````, а в скелете канона — внутри чужой, объемлющей ограды,
|
||||
и своей там иметь не должно. Пересборка берёт тело дома без крайних оград и
|
||||
надевает обратно ту, что была у копии; пустые строки по краям — так же.
|
||||
|
||||
## Проверка адресов документов
|
||||
|
||||
Адрес документа принадлежит одному плагину, а называют его все: `docs/*` стоит
|
||||
примерно в сорока местах конвейера, `tasks/ROADMAP.md` — в четырёх местах канона.
|
||||
Переименование в каноне до этих мест само не доходит.
|
||||
|
||||
```
|
||||
python3 scripts/addresses.py # весь репозиторий
|
||||
# 0 сошлось, 1 упразднённый адрес или опечатка, 3 перечень владельца недоступен
|
||||
```
|
||||
|
||||
**Зачем машина, а не аккуратность.** Прогон ревью умеет честно деградировать:
|
||||
дома темы нет — в границах покрытия появляется строка «документа в проекте нет»
|
||||
с названной ценой. Протухший адрес попадает ровно в эту машинерию и выходит
|
||||
**правдоподобным отчётом**, а не поломкой. Громкий признак ошибки деградацией
|
||||
убран, и здесь он возвращается гейтом.
|
||||
|
||||
Перечень берётся из **константы владельца** — той, по которой он и так проверяет
|
||||
раскладку (`docs.py`, `tasks.py`). Второй перечень прозой был бы вторым домом
|
||||
ровно того сорта, против которого написан канон.
|
||||
|
||||
Судится **упразднённое, а не незнакомое**, и это следует из канона: список тем
|
||||
открытый, всё, что проект кладёт в `docs/` сверх закрытых категорий, — законная
|
||||
тема, и опровергнуть её нечем. Зато переименование ловится точно: канон, убирая
|
||||
слот, кладёт его в карту переездов, и она здесь и есть перечень запрещённого.
|
||||
Рядом единственная догадка — имя, **почти** совпавшее с каноническим: `securty`
|
||||
это опечатка вероятнее, чем новая тема. Порог замерен по репозиторию: законные
|
||||
имена дают до 0.64, опечатки — от 0.91.
|
||||
|
||||
Не проверяются журналы (они описывают прошлые состояния и задним числом не
|
||||
переписываются), адреса `openspec/*` (раскладка чужого инструмента, владельца у
|
||||
нас нет) и упоминания в комментариях скриптов — сверяется только markdown. Эти
|
||||
границы скрипт печатает сам.
|
||||
|
||||
## Проверка диаграмм
|
||||
|
||||
Диаграммы `mermaid` живут исходником в markdown — картинок в репозитории нет.
|
||||
@@ -322,8 +450,8 @@ uv run python scripts/copies.py # 0 сошлось, 1 расхождение
|
||||
правдоподобно, диff показывает разумную строку, а рендер падает.
|
||||
|
||||
```
|
||||
uv run python scripts/diagrams.py # весь репозиторий
|
||||
uv run python scripts/diagrams.py A.md B.md # только названные файлы
|
||||
python3 scripts/diagrams.py # весь репозиторий
|
||||
python3 scripts/diagrams.py A.md B.md # только названные файлы
|
||||
# 0 рендерятся, 1 нет, 3 нет mermaid-cli
|
||||
```
|
||||
|
||||
@@ -344,7 +472,7 @@ uv run python scripts/diagrams.py A.md B.md # только названные
|
||||
|
||||
## Гейт коммита
|
||||
|
||||
Все пять проверок стоят в `pre-commit` через [lefthook](https://lefthook.dev) —
|
||||
Все шесть проверок стоят в `pre-commit` через [lefthook](https://lefthook.dev) —
|
||||
конфиг в [lefthook.yml](lefthook.yml), ставится один раз на клон:
|
||||
|
||||
```
|
||||
@@ -354,8 +482,9 @@ lefthook run pre-commit # прогнать руками, не коммитя
|
||||
|
||||
| Проверка | Когда идёт | Что смотрит | Сколько |
|
||||
| --- | --- | --- | --- |
|
||||
| фронтматтеры | правка `*.md` | весь репозиторий | миллисекунды |
|
||||
| фронтматтеры | правка `*.md` или `*.json` | весь репозиторий | миллисекунды |
|
||||
| копии правил | правка `*.md` | весь репозиторий | миллисекунды |
|
||||
| адреса документов | **каждый коммит** | весь репозиторий | ~0.07 с |
|
||||
| диаграммы | правка `*.md` | staged-файлы | ~1 с на файл |
|
||||
| `ruff check --fix` | правка `*.py` | staged-файлы | доли секунды |
|
||||
| `pyrefly check` | правка `*.py` | staged-файлы | доли секунды |
|
||||
@@ -364,15 +493,21 @@ Glob разводит две половины: коммит, трогающий
|
||||
диаграмм, а коммит в документы не гоняет линтеры.
|
||||
|
||||
**Судятся staged-файлы, а не рабочее дерево** — гейт обязан проверять то, что
|
||||
уедет в историю, а не то, что случайно лежит рядом на диске. Исключений два, и
|
||||
оба про существо, а не про удобство: `copies.py` сверяет копию с домом, а дом
|
||||
лежит в другом файле, которого в индексе может не быть (список staged дал бы
|
||||
«копии дословны» ровно там, где правка дома их и разошлась), а `frontmatter.py`
|
||||
обходит весь репозиторий за сотые доли секунды — экономить тут нечего.
|
||||
уедет в историю, а не то, что случайно лежит рядом на диске. Исключений три, и
|
||||
все про существо, а не про удобство: `copies.py` сверяет копию с домом, а
|
||||
дом лежит в другом файле, которого в индексе может не быть (список staged дал бы
|
||||
«копии дословны» ровно там, где правка дома их и разошлась); `frontmatter.py`
|
||||
обходит весь репозиторий за сотые доли секунды — экономить тут нечего;
|
||||
`addresses.py` идёт **без glob вовсе**, потому что сводит две стороны: перечень
|
||||
адресов лежит в `*.py` владельца, а упоминания — в `*.md` соседей, и коммит с
|
||||
переименованием документа трогает только первую.
|
||||
|
||||
**`ruff` чинит безопасное сам, и починка доносится до этого же коммита**
|
||||
(`stage_fixed: true`). Иначе исправленный файл остался бы в рабочем дереве, а в
|
||||
историю уехал бы невычищенный — худший из исходов: гейт зелёный, коммит грязный.
|
||||
|
||||
**`resync.py` в гейте нет намеренно** — он чинит, а не проверяет, и его правка
|
||||
обязана быть прочитана глазами (см. выше).
|
||||
|
||||
**Обход разовый — `LEFTHOOK=0 git commit …`.** Он законен ровно для случая,
|
||||
когда найденное нечем чинить прямо сейчас; молча пропущенная проверка — нет.
|
||||
|
||||
+35
-23
@@ -36,13 +36,14 @@ severity. Пробы готовы и синтетических не нужно
|
||||
но работает ли обязанность, не проверено. Оркестратор реагирует на severity,
|
||||
поэтому цена — не «не найдём», а **«найдём и не починим»**.
|
||||
|
||||
Сама работа — [TODO.md](TODO.md), раздел 3; здесь только цена: замер стоит
|
||||
перед переездом jellybit и блокирует его (решение 39), а ожидаемый исход уже
|
||||
назван выше.
|
||||
Сама работа — [TODO.md](TODO.md), раздел «Калибровка»; здесь только цена: замер
|
||||
стоит перед переездом jellybit и блокирует его (решение 39), а ожидаемый исход
|
||||
уже назван выше.
|
||||
|
||||
**«Главный» здесь про цену, а не про очередь.** Первой идёт адаптация healthlog
|
||||
(TODO, раздел 2): без неё нет проекта под каноном, на котором работают остальные
|
||||
скиллы. Калибровка блокирует один шаг — переезд jellybit, — а не всё подряд.
|
||||
(TODO, раздел «Живые проекты»): без неё нет проекта под каноном, на котором
|
||||
работают остальные скиллы. Калибровка блокирует один шаг — переезд jellybit, — а
|
||||
не всё подряд.
|
||||
|
||||
## Что ещё не сделано
|
||||
|
||||
@@ -51,12 +52,16 @@ severity. Пробы готовы и синтетических не нужно
|
||||
|
||||
- **Ни один скилл не прогонялся на живом проекте.** `docs.py` прогнан на
|
||||
healthlog и jellybit в режиме `check` и находит осмысленный дрейф; `init`,
|
||||
`canon adopt`, `canon upgrade` и скилл `docs` не исполнялись ни разу.
|
||||
`canon adopt`, `canon upgrade`, скиллы `docs`, `openspec` и `resolve` не
|
||||
исполнялись ни разу. `openspec.py`, раскол плагинов и оба чекпоинта `resolve`
|
||||
проверены только на фикстурах и на установке каждого плагина в одиночку.
|
||||
- **Проектные копии в healthlog и jellybit.** Два `.claude/skills/` и одиннадцать
|
||||
`.claude/agents/` старого поколения. У jellybit хуже: его скиллы названы
|
||||
`task-pipeline`, `review-pipeline`, `task-batch` — **ровно как в плагине**.
|
||||
Claude Code не переопределяет их, а держит обе пары, так что короткое имя может
|
||||
увести в устаревшую копию, и молча.
|
||||
`.claude/agents/` старого поколения — их надо снести при установке.
|
||||
**Совпадение имён при этом больше не грозит:** скиллы jellybit названы
|
||||
`task-pipeline`, `review-pipeline`, `task-batch`, а плагин теперь даёт
|
||||
`resolve`, `review`, `openspec` — ни одно имя не пересекается. Риск снят
|
||||
переименованием, а не устранён по существу: заведись у проекта свой `review`,
|
||||
Claude Code держал бы обе пары, и короткое имя увело бы в копию молча.
|
||||
|
||||
## Открытые вопросы
|
||||
|
||||
@@ -76,10 +81,11 @@ check` сверяет версию, но не то, что миграционн
|
||||
только её последствия.
|
||||
|
||||
**Не выродились ли «границы покрытия» в шаблон.** Строка «что смотрели и чего не
|
||||
смотрели» обязательна в докладе `check`, `adopt`, интейка, штурма, сессии,
|
||||
спринта и всех четырёх агентов — семь и больше раз за сессию, и проверить её
|
||||
исполнение некому: приёмщик и исполнитель одно лицо (`session/SKILL.md`,
|
||||
«Стимулы»). Выродившаяся строка **хуже отсутствия**: доклад выглядит проверенным.
|
||||
смотрели» обязательна в докладе `check`, `adopt`, интейка, штурма и всех пяти
|
||||
агентов канона и задач (`doc-consistency`, `doc-code-drift`, `doc-wording`,
|
||||
`task-form`, `task-wording`) — девять мест, и проверить её исполнение некому:
|
||||
приёмщик и исполнитель одно лицо (`groom/SKILL.md`, «Стимулы»). Выродившаяся
|
||||
строка **хуже отсутствия**: доклад выглядит проверенным.
|
||||
|
||||
Приём не правится: это гипотеза об износе, а не находка, и менять работающее по
|
||||
догадке дороже. **Наблюдение к первой обкатке на живом проекте:** если в трёх
|
||||
@@ -88,10 +94,11 @@ check` сверяет версию, но не то, что миграционн
|
||||
|
||||
**Форма ADR при пересмотре решения.** Парный статус («старая запись получает
|
||||
`заменено на`») судит агент `doc-consistency` — правило 6 его устава. Охват был
|
||||
открытым вопросом, пока агент зовётся пачкой, отобранной работой; переезд вызова
|
||||
на сессию с пачкой «весь канон» его снял. Остаётся зазор в спринт и отсутствие
|
||||
механической проверки — то есть пересмотр, сделанный сегодня, судится на
|
||||
ближайшей сессии, а не в момент правки.
|
||||
открытым вопросом, пока агент звался пачкой, отобранной работой; переезд вызова
|
||||
в `av-dev-docs:healthcheck` с пачкой «весь канон» его снял. Остаётся зазор до
|
||||
ближайшего прогона `healthcheck` и отсутствие механической проверки — то есть
|
||||
пересмотр, сделанный сегодня, судится тогда, когда позовут сверку, а не в момент
|
||||
правки.
|
||||
|
||||
## Известные пределы — приняты, чинить не планируется
|
||||
|
||||
@@ -111,14 +118,19 @@ check` сверяет версию, но не то, что миграционн
|
||||
словарь строится каждый раз заново из спек и архитектуры. Цена не измерена.
|
||||
|
||||
**Смысловые дубли ловит только агент.** `docs.py` видит раскладку, но не то, что
|
||||
`docs/specs/recognition.md` описывает то же, что capability `recognition`.
|
||||
раздел `docs/architecture.md` описывает поведение, уже записанное capability
|
||||
`recognition`.
|
||||
Граница объявляется вслух в каждом отчёте — это единственная защита от
|
||||
«соблюдено» на проекте с тремя лишними файлами.
|
||||
|
||||
**Приёмщик и исполнитель совпали.** Граница «пайплайн не закрывает задачу» снята
|
||||
сознательно (решение P); три защиты из раздела «Стимулы» держатся теперь текстом,
|
||||
а не механикой. Реальные опоры — сохранённый отчёт триажа, `SPRINT.md` под git и
|
||||
`reopen`. Это записано в самом скилле, а не спрятано.
|
||||
**Приёмщик и исполнитель совпали, и опор стало меньше.** Граница «пайплайн не
|
||||
закрывает задачу» снята сознательно (решение P); защиты держатся текстом, а не
|
||||
механикой. Реальных опор было три, осталось две: сохранённый отчёт триажа и
|
||||
`reopen` (индексы под git показывают закрытие, потому что оно коммитится
|
||||
отдельным коммитом учёта). Третья — приёмка шагом сессии — ушла вместе со
|
||||
спринтами: у неё больше **нет момента**, и происходит она только тогда, когда
|
||||
что-то бросилось в глаза на груминге. Это записано в самих скиллах, а не
|
||||
спрятано.
|
||||
|
||||
**Копия правила в шаблонах проекта.** `adr/README.md` и `review.md` уезжают в
|
||||
репозиторий и обязаны там что-то говорить, поэтому правило канона в них
|
||||
|
||||
@@ -1,227 +1,118 @@
|
||||
# Работы по итогам разбора
|
||||
# Что осталось сделать
|
||||
|
||||
Порядок и обоснование — [DECISIONS.md](DECISIONS.md), тема 8. Номера в скобках —
|
||||
следствия оттуда.
|
||||
**Здесь только работы и их порядок.** Чего здесь нет намеренно:
|
||||
|
||||
Замер (шаг 3) — **единственный шаг, который нельзя переставить**: он блокирует
|
||||
переезд jellybit. Всё остальное можно тасовать.
|
||||
- **риски, открытые вопросы и принятые пределы** — [REMAINING.md](REMAINING.md);
|
||||
- **почему решено так** — [DECISIONS.md](DECISIONS.md), записи датированы;
|
||||
- **шаги повышения проекта с версии канона на версию** — журнал версий
|
||||
([changelog.md](av-dev-docs/skills/canon/references/changelog.md)). Пересказ их
|
||||
сюда был бы вторым домом, и прежний план на этом уже разъезжался: он повторял
|
||||
записи версий 3, 4 и 5 построчно, и половина повторов протухла молча.
|
||||
|
||||
## 0. Предусловие
|
||||
Сделанное отсюда **удаляется, а не помечается галочкой**. След остаётся в
|
||||
коммитах и в `DECISIONS.md`; список из двух сотен `[x]` перестают читать целиком,
|
||||
и живые пункты в нём теряются — прежний план умер именно так.
|
||||
|
||||
- [x] `git push` — `092d07c..88c5d97`, 17 коммитов ушли на origin (37)
|
||||
- [x] `claude plugin marketplace update av-dev-skills` — клон встал на `88c5d97`
|
||||
и видит `av-dev-pm` и `av-dev-pipeline`
|
||||
## Где мы сейчас
|
||||
|
||||
## 1. Репозиторий плагинов
|
||||
Плагинов четыре, и каждый ставится отдельно: `av-dev-docs` (канон документов и
|
||||
их содержимое), `av-dev-tasks` (задачи и цели), `av-dev-code` (код по задачам:
|
||||
цикл SDD, конвейер ревью, OpenSpec), `av-dev-git`. Общее, что нужно нескольким
|
||||
дословно, живёт домом в `shared/` и уезжает копиями.
|
||||
|
||||
### 1.1 Переименование
|
||||
Канон документов — **версия 12**. Живые проекты стоят на 2–3 и на плагине
|
||||
`av-dev-pm`, которого больше нет.
|
||||
|
||||
- [x] `av-dev-tasks` → `av-dev-pm`: каталог, `plugin.json`, `marketplace.json` (18)
|
||||
- [x] пространство имён во всех текстах: `av-dev-tasks:session` →
|
||||
`av-dev-pm:session`, включая ссылку из `task-pipeline` (18)
|
||||
Бумажная часть закрыта аудитом четырёх плагинов и четырьмя пропусками правок
|
||||
(DECISIONS, запись 59). Всё, что ниже, проверяется **только на живом коде**.
|
||||
|
||||
### 1.2 Канон — единственный дом определения
|
||||
## 1. Живые проекты — вернуть в рабочее состояние
|
||||
|
||||
- [x] `av-dev-pm/skills/canon/references/canon.md` — раскладка, роли документов,
|
||||
правило единственного дома. Читают `init`, `canon`, `docs` (AA)
|
||||
- [x] `av-dev-pm/skills/canon/references/changelog.md` — журнал версий канона,
|
||||
версия 1 (26)
|
||||
Блокирует всё остальное: под текущим каноном не стоит ни один проект, и ни один
|
||||
скилл, кроме `docs.py check`, не исполнялся на живом коде ни разу
|
||||
(см. REMAINING, «Что ещё не сделано»).
|
||||
|
||||
### 1.3 Правки существующих скиллов
|
||||
### healthlog — первым
|
||||
|
||||
- [x] `tasks`: убрать слот 6 «Команда учёта задач» (33)
|
||||
- [x] `tasks`: путь каталога жёсткий `docs/tasks`, убрать цепочку разрешения (F)
|
||||
- [x] `tasks`: `.tasks.json` → `docs/.pm.json`, там же версия канона и путь
|
||||
миграций (23, 30)
|
||||
- [x] `tasks`: убрать слоты 3 «куда переезжает суть» и 5 «оракулы» — отвечает
|
||||
канон и семантика гейта (тема 4)
|
||||
- [x] `session`: убрать слот 7 и слот 4 «где живёт разбор процесса» (33, K)
|
||||
- [x] `session`: переписать «Стимулы, которые процесс создаёт» — снятая граница
|
||||
выбила опору у трёх защит (19)
|
||||
|
||||
### 1.4 Новые скиллы
|
||||
|
||||
- [x] `init` — интервью по брифу → канон нового проекта (R)
|
||||
- [x] `canon` — `check` / `adopt` / `upgrade`; поглощает скилл `adopt` (R, 21, 24)
|
||||
- [x] `docs` — содержимое канона: ADR из архивного `design.md`, промоут
|
||||
конвенций, запись в `research/` и `review.md`, чистка `architecture.md` (X)
|
||||
- [x] `docs.py` — раскладка, лишние файлы, битые ссылки, версия, плейсхолдеры,
|
||||
маркеры долга; сверки миграции ↔ `database.md` и capability ↔
|
||||
`architecture.md` (T, 31)
|
||||
|
||||
### 1.5 av-dev-pipeline
|
||||
|
||||
- [x] удалить скилл `project-brief` и `references/{project-brief,brief-template}.md` (13)
|
||||
- [x] снять ветки деградации OpenSpec в трёх местах: `task-pipeline`,
|
||||
`review-pipeline`, `task-batch` (1)
|
||||
- [x] девять charter'ов: разделы брифа → пути канона; `ops`/`adversary`/`reimpl`
|
||||
обязаны сшивать `research/` и `database.md` (14, 15)
|
||||
- [x] `review-pipeline`: убрать бриф, поразрядная деградация по документам (16)
|
||||
- [x] `task-pipeline` шаг 9 → построчный доклад по документам канона (28)
|
||||
- [x] `task-pipeline`/`task-batch`: закрытие задачи вызовом скилла
|
||||
`av-dev-pm:tasks`, слот убрать (32, 33)
|
||||
- [x] `promote.md` шаг 3: перечень механизированного → `conventions/README.md` (29)
|
||||
- [x] описание плагина: «требует OpenSpec» (2)
|
||||
|
||||
### 1.6 Прочее
|
||||
|
||||
- [x] `av-dev-backlog` — пометить устаревшим, переписать описание, чтобы не
|
||||
ловило триггер (Q)
|
||||
- [x] `README.md` маркетплейса — три плагина, канон, установка
|
||||
- [x] `HISTORY.md` — сжать `AGENTIC-TASKS.md` до истории решений (CC)
|
||||
- [x] `REMAINING.md` пересобрать: пункт 2 отменён, четыре вопроса закрыты,
|
||||
калибровка стала обязательной (38)
|
||||
|
||||
### 1.7 Линтеры скриптов (тема 9)
|
||||
|
||||
- [x] `pyproject.toml`: ruff + pyrefly через `uv`, версии прибиты (DD, FF)
|
||||
- [x] запрет внешних зависимостей двумя способами: `banned-api` + пустое
|
||||
окружение pyrefly (EE)
|
||||
- [x] `RUF001`–`RUF003` выключены, `av-dev-backlog` исключён (GG, HH)
|
||||
- [x] починены 27 находок ruff и 14 pyrefly; `os` из `tasks.py` ушёл (40, 41)
|
||||
- [x] раздел «Проверка скриптов» в `README.md`
|
||||
|
||||
### 1.8 Ревью двумя проходами (тема 10)
|
||||
|
||||
- [x] `reopen` берёт текст из `HEAD`, когда коммита удаления ещё нет (JJ)
|
||||
- [x] шаг 11 коммитит учёт вторым коммитом; батч проверяет чистоту дерева (JJ)
|
||||
- [x] фиктивный ключ `tasks.sections` убран из четырёх документов (KK)
|
||||
- [x] `init` пишет конфиг в `docs/.pm.json`; `looks_like_tasks` его читает
|
||||
- [x] урожай спринта заводится до `sprint close`, слаг — из его отчёта
|
||||
- [x] ответ на вопрос опустошает раздел «Вопросы» — во всех трёх местах
|
||||
- [x] путь отчёта триажа переживает архивацию (5 мест)
|
||||
- [x] `review-specs` получил режим 3 — стык после слияния
|
||||
- [x] остальные 12 находок: `sprint.md`, «9а», перечень проектных копий,
|
||||
параллельность в батче, триаж в финальной сверке, триггеры профиля, 8–12
|
||||
|
||||
### 1.9 Ревью зависимостей между плагинами (тема 11)
|
||||
|
||||
- [x] опоры приёмки названы абстрактно, деградация без конвейера объявлена (MM)
|
||||
- [x] ветка деградации шага 9 ходит в свой `project-facts.md` (NN)
|
||||
- [x] `docs` даёт ветку «конвейера нет» для журнала и промоута
|
||||
- [x] форма журнала дефектов сведена к дому, копия помечена в `changelog.md`
|
||||
- [x] `specs` вернулся в читатели `docs/research/`; дом списка назначен
|
||||
- [x] пайплайн не называет `items/` и `SPRINT.md` — их знает `av-dev-pm`
|
||||
- [x] манифесты объявили `av-dev-pm` опциональным для конвейера (49)
|
||||
|
||||
### 1.10 Механическая проверка копий (тема 12)
|
||||
|
||||
- [x] `scripts/copies.py`: маркеры дома и копии, побайтовая сверка (OO)
|
||||
- [x] строгий id, повторяемый в закрывающем маркере (PP)
|
||||
- [x] помечены два контракта; «когда заводить ADR» сведён к дословному (50)
|
||||
- [x] раздел «Проверка копий правил» в `README.md`, правило — в обоих домах
|
||||
|
||||
## 2. healthlog — первая боевая проверка
|
||||
|
||||
- [ ] `canon adopt`; `docs/backlog/` → `docs/tasks/`
|
||||
- [ ] `architecture.md` 1662 строки → обзор, остаток маркерами (W)
|
||||
- [ ] после выноса поведения — замерить остаток `architecture.md`; **решать
|
||||
больше нечего**: канон 5 разрешил любой теме быть каталогом с `README.md`,
|
||||
так что жмёт — заводи `docs/architecture/`, и это не смена версии
|
||||
(тема 16, GGG, 65; закрыто темой 36)
|
||||
- [ ] завести `security.md` с периметром первой строкой (J)
|
||||
- [ ] `review-journal.md` → `review.md` + настройка конвейера (K, L)
|
||||
- [ ] `conventions.md` → `conventions/`, `local-research.md` → `research/` (G)
|
||||
- [ ] `plan.md` → `docs/tasks/ROADMAP.md` (E)
|
||||
- [ ] завести `docs/adr/`
|
||||
- [ ] `CLAUDE.md`: severity инвариантов, семантика гейта, убрать раздел
|
||||
«Процесс» (M, N)
|
||||
- [ ] `docs.py check` в `task gate` (V)
|
||||
- [ ] почистить `openspec/config.yaml` (C)
|
||||
- [ ] переустановить плагины: снять `av-dev-pm` и `av-dev-pipeline`, поставить
|
||||
`av-dev-docs`, `av-dev-tasks`, `av-dev-code`, `av-dev-git`. Оба прежних
|
||||
имени мертвы, и `plugin update` их не переименует — только снять и
|
||||
поставить. `marketplace update`, затем `plugin update` — одного шага мало
|
||||
(README, «Обновление»)
|
||||
- [ ] удалить проектные копии: `.claude/skills/healthlog-{task,review}-pipeline`
|
||||
и девять `.claude/agents/healthlog-review-*.md` — они прошлого поколения и
|
||||
после переезда указывают на `docs/conventions.md`, `docs/local-research.md`,
|
||||
`docs/review-journal.md`, которых уже не будет
|
||||
и девять `.claude/agents/healthlog-review-*.md`. Они прошлого поколения и
|
||||
после переезда указывают на документы, которых уже не будет
|
||||
- [ ] `av-dev-docs:canon` в режиме `adopt` — он приведёт проект к канону 12
|
||||
сразу, картой и с подтверждением. Файл-в-файл здесь не расписан: раскладку
|
||||
знает скилл, и второй перечень разошёлся бы с ним
|
||||
- [ ] каталог задач — в `tasks/` корня (канон 11), не в `docs/tasks/`, и без
|
||||
`SPRINT.md` (канон 12). Скилл задач зовётся из `adopt` сам
|
||||
- [ ] гейт проекта: три шага вместо одного — `docs.py check`, `tasks.py check
|
||||
--dir tasks`, `openspec.py check`. **Второй и третий раньше не были
|
||||
нужны:** согласованность задач тянул за собой `docs.py`, форму `config.yaml`
|
||||
он же. Теперь оба молчат, и без своих шагов дрейф перестанет ловиться
|
||||
- [ ] разобрать урожай `doc-consistency` и `doc-code-drift` порциями — правило
|
||||
единственного дома на живом проекте не проверял никто
|
||||
|
||||
## 3. Калибровка — блокирует шаг 5
|
||||
### jellybit — после калибровки
|
||||
|
||||
Порядок не произволен: замер (раздел 3) блокирует переезд jellybit, и только его.
|
||||
|
||||
- [ ] то же, что у healthlog: плагины, проектные копии, `adopt`, каталог задач,
|
||||
гейт
|
||||
- [ ] проектные копии здесь опаснее: скиллы названы `task-pipeline`,
|
||||
`review-pipeline` — **ровно как в плагине**, и короткое имя
|
||||
может увести в устаревшую копию молча (REMAINING)
|
||||
|
||||
## 2. Учёт работ без спринтов — что осталось
|
||||
|
||||
Сделано: спринт снят со скрипта и текстов, приоритет стал порядком строк в
|
||||
беклоге, гейт готовности переехал в `tasks.py ready`, `session` стал скиллом
|
||||
`groom`, запись 12 в журнал версий канона написана.
|
||||
|
||||
- [ ] прогнать груминг на живом беклоге — на фикстуре проверялись команды, а не
|
||||
сам разбор. Наблюдение к первому прогону: **не выродился ли шаг 4 в
|
||||
«оставить как есть»** — признак тот, что доклад не называет ни одного
|
||||
движения с доводом
|
||||
|
||||
## 3. Калибровка — блокирует переезд jellybit
|
||||
|
||||
- [ ] замер на четырёх находках healthlog: скелет из `null`, откат бинаря,
|
||||
канонизация в транзакции, `-1 >= -1` (1 из REMAINING, 14)
|
||||
канонизация в транзакции, `-1 >= -1`. Цена и ожидаемый исход — REMAINING,
|
||||
«Главный незакрытый риск»
|
||||
|
||||
## 4. Обкатка
|
||||
## 4. Конвейер: что осталось после `resolve`
|
||||
|
||||
- [ ] один-два спринта healthlog на новом процессе
|
||||
Сам скилл написан (`av-dev-code:resolve`, два чекпоинта, ветка разведки),
|
||||
`task-batch` удалён. Осталось то, что на бумаге не проверяется:
|
||||
|
||||
## 5. jellybit
|
||||
- [ ] перемерить скилл `review` тем же вопросом, что и проект целиком:
|
||||
сколько из его стадий реально смотрятся глазами. Тысяча строк, и весь
|
||||
автоматический участок между чекпоинтами держится на них
|
||||
- [ ] чекпоинт «объяснение» собирается из `proposal.md` и `design.md`, а
|
||||
требования к их форме уехали в `openspec/config.yaml` (`rules.proposal`,
|
||||
`rules.design`). **На живом проекте это ни разу не работало:** неизвестно,
|
||||
хватает ли двух артефактов, чтобы объяснение не пришлось дописывать руками
|
||||
|
||||
- [ ] `BRIEF.md` → `docs/passport.md`, обновить
|
||||
- [ ] `docs/specs/{recognition,review-ux,workflow}.md` — сверить с capability и
|
||||
удалить как дубли (10)
|
||||
- [ ] `docs/specs/architecture.md` → `docs/architecture.md`, `database.md` →
|
||||
`docs/database.md`, `jellyfin-layout.md` → `docs/research/`
|
||||
- [ ] `docs/review/journal.md` → `docs/review.md`
|
||||
- [ ] `drafts/` растворить: roadmap → `ROADMAP.md`, conventions-backlog → записи
|
||||
`research` (сырьё: тип есть, «Вопрос» пуст), logical-title-model → ADR (H)
|
||||
- [ ] `docs/backlog/` → `docs/tasks/`
|
||||
- [ ] удалить проектные копии скиллов и агентов (4 из REMAINING)
|
||||
- [x] `av-dev-backlog` удалить из маркетплейса и снять с проекта (тема 30)
|
||||
## 5. Мелочь, оставленная аудитом сознательно
|
||||
|
||||
## 6. Канон версии 3 — повысить живые проекты (тема 17)
|
||||
Одной пачкой, когда будет повод открыть эти файлы, — не раньше:
|
||||
|
||||
Оба проекта стоят на каноне 2 и держат `docs/tasks/PLAN.md`: healthlog 55 задач,
|
||||
jellybit 43. Шаги повышения — [changelog.md](av-dev-pm/skills/canon/references/changelog.md),
|
||||
запись «Версия 3»; делаются скиллом `av-dev-pm:canon` в режиме `upgrade`.
|
||||
- [ ] «чекпоинт» несёт третий смысл — точка наблюдаемости в коде
|
||||
(`finding-contract.md`, `promote.md`). Слово занято дважды по своему же
|
||||
правилу, но домены разные, и переименование здесь может выйти дороже
|
||||
путаницы
|
||||
- [ ] закрытый словарь `shared/language.md` не содержит ни «конвейера», ни
|
||||
«чекпоинта», ни «груминга» — трёх рабочих терминов репозитория. Список
|
||||
объявлен закрытым, и пополнять его на ходу нельзя
|
||||
- [ ] `move <слаг>` без флагов теперь легален и значит «в конец своей секции» —
|
||||
осмысленная операция, но в прозе не описана нигде
|
||||
- [ ] `reopen` печатает «позиция это приоритет» и для целей роадмапа, где секции
|
||||
очередью не являются
|
||||
|
||||
- [x] healthlog: `PLAN.md` → `ROADMAP.md`, ссылки, `"canon": 3` — сделано,
|
||||
лежит в рабочем дереве проекта некоммитнутым
|
||||
- [ ] jellybit: то же
|
||||
- [ ] род работы и раздел «Затрагивает» — **не задним числом**: сперва то, что
|
||||
идёт в ближайший набор (`sprint take` без них откажет), остальное по ходу
|
||||
переоценки (PPP)
|
||||
- [ ] секции роадмапа: `порядок` → `Запланировано`, `темы` → `Направления`,
|
||||
завести `Готово` и `Сопровождение`; прозаические разделы healthlog («Что уже
|
||||
пройдено», «Почему в таком порядке») разложить — звенья строками в
|
||||
`Готово`, обоснование очереди прозой внутри `Запланировано` (тема 19, 80).
|
||||
`check` теперь называет чужую секцию ошибкой, так что шаг обязателен
|
||||
- [ ] переформулировать цели ответом на «что приложение будет уметь»; цели не
|
||||
про приложение («Процесс и качество разработки» в jellybit) — в
|
||||
`Сопровождение`
|
||||
- [ ] `check --fix` на обоих: поднимет написание канонических секций, поставит
|
||||
отбивку после заголовков и сведёт секцию в мете файлов с заголовками.
|
||||
Секции беклога переименовать руками — имена выбирал проект (тема 20, ККК)
|
||||
- [ ] заголовки задач в форму действия — **не задним числом**: по мере попадания
|
||||
задачи в работу. `check` печатает их число, `task-form` предложит
|
||||
формулировки пачкой (тема 20, ЕЕЕ)
|
||||
## 6. Обкатка
|
||||
|
||||
**Канон 4** — сверх того (changelog, запись «Версия 4»):
|
||||
|
||||
- [ ] healthlog: `## Разработка` → `## Сопровождение`, поле «Секция» в целях этой
|
||||
секции, `check --fix` (переставит `Готово` вниз и поправит отбивку),
|
||||
`"canon": 4`
|
||||
- [ ] jellybit едет сразу на 4: `Готово` заводить **последней**, секцию
|
||||
сопровождения — сразу с новым именем, переставлять дважды не нужно
|
||||
- [ ] типы: `check --fix` переведёт `kind:`/`[goal]`/`[idea]` в поле «Тип», снимет
|
||||
тег, поставит эмодзи, переименует «Секция» → «Категория» у задач и снесёт
|
||||
сырьё в конец категорий — **за один проход, вместе с порядком секций**
|
||||
- [ ] разобрать `НЕОДНОЗНАЧНО` после `--fix`: записи без типа (заведены до
|
||||
появления рода работы) машина не угадывает — `edit <слаг> --type …`
|
||||
- [ ] имена файлов: `docs.py check` назовёт кириллицу, не-kebab-case и форму
|
||||
имени ADR. Переименование ADR — **перенос ссылок одним проходом**: слаг
|
||||
стоит в `adr/README.md`, в `architecture.md` и в чужих документах
|
||||
- [ ] первый прогон `doc-consistency` на живом проекте — правило единственного
|
||||
дома до сих пор не проверял никто, урожай ожидается крупный; разбирать
|
||||
порциями
|
||||
- [ ] `doc-code-drift` — на ближайшей сессии между спринтами, с разделом
|
||||
запретов `CLAUDE.md` на входе
|
||||
- [ ] новые обязательные разделы — **не задним числом**: `Воспроизведение` у
|
||||
каждого `fix` и `Вопрос` + `Куда ляжет ответ` у каждого `research` пишутся
|
||||
по мере того, как задача идёт в набор (`sprint take` без них откажет).
|
||||
Сколько записей готово к взятию, печатает блок здоровья `check`
|
||||
- [ ] `docs/review.md`, «Триггеры профиля» — переписать целиком: снести перечень
|
||||
мест для `deep` (профиль упразднён), а оставшийся перевести на новое
|
||||
правило — `wide` это крупное или незнакомое изменение, 5–10% задач, плюс
|
||||
отдельный список мелкого для `quick`. Там же две честные строки в
|
||||
«перестали проверять сознательно»: форма решения (снят проход независимой
|
||||
реализации) и всё, что требует запуска (меряющие проходы только в `wide`)
|
||||
|
||||
**Канон 5** — сверх того (changelog, запись «Версия 5»):
|
||||
|
||||
- [ ] `docs/review.md`: «Вопросы к проходам» → **«Вопросы по темам»**, каждый
|
||||
вопрос переадресовать теме вместо имени прохода (`requirements`,
|
||||
`autotests`, `conventions`, `architecture`, `security`, `operations` плюс
|
||||
свои). «Недоступно проверке» — тоже разнести по темам
|
||||
- [ ] проверить, не просился ли в `docs/` документ, который раньше считался
|
||||
лишним: теперь он законен и **становится темой ревью**. Это единственный
|
||||
способ добавить проверку, которой в конвейере нет
|
||||
- [ ] `"canon": 5` в `docs/.pm.json` обоих проектов; форму домов не трогать —
|
||||
обе законны
|
||||
- [ ] один-два цикла healthlog на новом процессе. Наблюдения к первой обкатке
|
||||
два: не выродились ли «границы покрытия» в шаблон (REMAINING, «Открытые
|
||||
вопросы») и **не превратился ли чекпоинт в ритуал одобрения** — признак
|
||||
тот же, дословно повторяющийся текст и согласие без единой правки
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "av-dev-code",
|
||||
"description": "Решение одной задачи от постановки до закрытия скиллом resolve — полный цикл Spec Driven Development с двумя плановыми остановками: чекпоинт вариантов у исследовательской задачи и чекпоинт с объяснением человеческим языком после ревью дизайна у всякой. Между ними работа идёт без согласований. Плюс конвейер ревью с детерминированным гейтом, сверкой со спеками, враждебными постановками, эксплуатационным постмортемом и обязательным триажем. Требует OpenSpec и сам его заводит скиллом openspec. Задача принимается и обычным текстом. Плагины av-dev-docs и av-dev-tasks опциональны: первый даёт документы канона, из которых проходы читают проектную конкретику, второй — учёт задач; без них прогон деградирует поразрядно и называет это строкой.",
|
||||
"author": {
|
||||
"name": "Anton Vakhrushev",
|
||||
"email": "anwinged@gmail.com"
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: review-adversary
|
||||
description: "Враждебный проход ревью — не проверяет свойства, а строит путь: «ты контролируешь вход целиком — выведи запись за пределы песочницы»; «ты шлёшь запрос и хочешь, чтобы данные не доехали или испортились — построй такой вход»; «ты можешь повторить и переставить любую операцию — что ломается»; «доведи чувствительное до места, где его быть не должно». Находка — построенный путь с шагами, а не наблюдение. Свойства без пути идут в отдельную секцию и не получают critical. Модель угроз берётся из docs/security.md проекта. Только чтение."
|
||||
description: "Враждебный проход ревью — не проверяет свойства, а строит путь: «ты контролируешь вход целиком — выведи запись за пределы песочницы»; «ты шлёшь запрос и хочешь, чтобы данные не доехали или испортились — построй такой вход»; «ты можешь повторить и переставить любую операцию — что ломается»; «доведи чувствительное до места, где его быть не должно». Находка — построенный путь с шагами, а не наблюдение. Свойства без пути идут в отдельную секцию и не получают critical. Модель угроз берётся из docs/security.md проекта. Запускается только с меткой large — на изменении, которое не крупное и не незнакомое, построенного пути он не находит, а стоит дорого. Только чтение."
|
||||
tools: Read, Grep, Glob, Bash
|
||||
model: opus
|
||||
color: yellow
|
||||
@@ -13,7 +13,7 @@ color: yellow
|
||||
равно опасен.
|
||||
|
||||
Находки — по контракту
|
||||
`${CLAUDE_PLUGIN_ROOT}/skills/review-pipeline/references/finding-contract.md`
|
||||
`${CLAUDE_PLUGIN_ROOT}/skills/review/references/finding-contract.md`
|
||||
(точный путь конвейер передаёт в задании).
|
||||
|
||||
**Ты помечен «держит машину»** — за тем, чтобы построенный путь можно было
|
||||
@@ -73,7 +73,7 @@ color: yellow
|
||||
находкой.
|
||||
|
||||
Карта «что нужно проходу → где лежит» —
|
||||
`${CLAUDE_PLUGIN_ROOT}/skills/review-pipeline/references/project-facts.md`.
|
||||
`${CLAUDE_PLUGIN_ROOT}/skills/review/references/project-facts.md`.
|
||||
|
||||
**Деградация поразрядная, и каждый пробел называется своей строкой.**
|
||||
`docs/security.md` нет — работай по общей рамке ниже, `critical` не присваивай и
|
||||
+2
-2
@@ -25,7 +25,7 @@ color: yellow
|
||||
и граф зависимостей есть только у тебя.
|
||||
|
||||
Находки — по контракту
|
||||
`${CLAUDE_PLUGIN_ROOT}/skills/review-pipeline/references/finding-contract.md`
|
||||
`${CLAUDE_PLUGIN_ROOT}/skills/review/references/finding-contract.md`
|
||||
(точный путь конвейер передаёт в задании).
|
||||
|
||||
## Вход (собери до чтения диффа)
|
||||
@@ -52,7 +52,7 @@ grep по именам концепций) и скажи об этом в гра
|
||||
- дельта-спеки change.
|
||||
|
||||
Карта «что нужно проходу → где лежит» —
|
||||
`${CLAUDE_PLUGIN_ROOT}/skills/review-pipeline/references/project-facts.md`.
|
||||
`${CLAUDE_PLUGIN_ROOT}/skills/review/references/project-facts.md`.
|
||||
|
||||
Дифф — **последним, не первым**: он должен ложиться на карту, а не задавать её.
|
||||
|
||||
@@ -19,7 +19,7 @@ color: green
|
||||
намеренно нет, из темы не выпадает — она уходит в границы покрытия.
|
||||
|
||||
Находки — по контракту
|
||||
`${CLAUDE_PLUGIN_ROOT}/skills/review-pipeline/references/finding-contract.md`
|
||||
`${CLAUDE_PLUGIN_ROOT}/skills/review/references/finding-contract.md`
|
||||
(точный путь конвейер передаёт в задании). Русская проза, идентификаторы и
|
||||
команды — в оригинале.
|
||||
|
||||
@@ -31,7 +31,7 @@ color: green
|
||||
запускать запрещено, с путями.
|
||||
|
||||
Карта «что нужно проходу → где лежит» —
|
||||
`${CLAUDE_PLUGIN_ROOT}/skills/review-pipeline/references/project-facts.md`.
|
||||
`${CLAUDE_PLUGIN_ROOT}/skills/review/references/project-facts.md`.
|
||||
|
||||
**Семантики гейта в `CLAUDE.md` нет** — найди команду сама (`Taskfile.yml`,
|
||||
`Makefile`, `justfile`, `scripts/`) и выполни её, но: `critical` по основанию
|
||||
@@ -96,7 +96,7 @@ color: green
|
||||
просило: она может стоить минут и трогать данные.
|
||||
- **Правило есть в конвенциях, но не в линтере.** Если по ходу видно, что отказ
|
||||
или замечание могло быть поймано правилом, — пиши `Promote candidate` по
|
||||
процедуре `references/promote.md`.
|
||||
процедуре `${CLAUDE_PLUGIN_ROOT}/skills/review/references/promote.md`.
|
||||
|
||||
## Что читать не нужно
|
||||
|
||||
@@ -36,7 +36,7 @@ color: yellow
|
||||
самый дорогой проход, вместо которого его позвали.
|
||||
|
||||
Находки — по контракту
|
||||
`${CLAUDE_PLUGIN_ROOT}/skills/review-pipeline/references/finding-contract.md`
|
||||
`${CLAUDE_PLUGIN_ROOT}/skills/review/references/finding-contract.md`
|
||||
(точный путь конвейер передаёт в задании).
|
||||
|
||||
## Что тебе даёт план прогона
|
||||
@@ -146,8 +146,8 @@ color: yellow
|
||||
**Молча отменённое решение ADR больше не проверяет никто, и это сознательно.**
|
||||
Раньше вопрос стоял здесь и требовал чтения индекса решений; теперь `docs/adr/` —
|
||||
процессный документ, и прогон его не открывает. Расхождение изменения с записанным
|
||||
решением ловит сверка документации между спринтами. Строка об этом обязательна в
|
||||
твоих границах покрытия.
|
||||
решением ловит сверка документации — скилл `av-dev-docs:healthcheck`. Строка об
|
||||
этом обязательна в твоих границах покрытия.
|
||||
|
||||
**Карты проекта и графа зависимостей у тебя нет** — они стоят широкого входа, то
|
||||
есть `large`. Твой вход — **дифф и его окрестности**. Греп по базе тебе разрешён
|
||||
@@ -52,7 +52,7 @@ color: yellow
|
||||
его неизбежным.
|
||||
|
||||
Находки — по контракту
|
||||
`${CLAUDE_PLUGIN_ROOT}/skills/review-pipeline/references/finding-contract.md`
|
||||
`${CLAUDE_PLUGIN_ROOT}/skills/review/references/finding-contract.md`
|
||||
(точный путь конвейер передаёт в задании). Русская проза, идентификаторы и пути —
|
||||
в оригинале. Читай реальный код, ничего не выдумывай.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: review-ops
|
||||
description: "Эксплуатационный проход ревью — пишет постмортем «это упало через неделю на проде» от симптома у владельца сервиса к строке кода. Обязательные вопросы: рост объёма, деградация окружения и внешних зависимостей, повторная и одновременная операция, частичный откат при двух версиях, миграция под живым потоком, отмена контекста на середине, наблюдаемость и тишина, поведение библиотеки и драйвера в вырожденном случае, чтение узлом состояния, которое он сам же меняет. Формулирует условиями, а не утверждениями — реального профиля нагрузки не знает. Только чтение."
|
||||
description: "Эксплуатационный проход ревью — пишет постмортем «это упало через неделю на проде» от симптома у владельца сервиса к строке кода. Обязательные вопросы: рост объёма, деградация окружения и внешних зависимостей, повторная и одновременная операция, частичный откат при двух версиях, миграция под живым потоком, отмена контекста на середине, наблюдаемость и тишина, поведение библиотеки и драйвера в вырожденном случае, чтение узлом состояния, которое он сам же меняет. Формулирует условиями, а не утверждениями — реального профиля нагрузки не знает. Запускается только с меткой large: постмортем на малом знакомом изменении пишется по общей практике, а не по этому проекту. Только чтение."
|
||||
tools: Read, Grep, Glob, Bash
|
||||
model: sonnet
|
||||
color: green
|
||||
@@ -11,7 +11,7 @@ color: green
|
||||
увидит владелец сервиса, и дойди до строки кода.
|
||||
|
||||
Находки — по контракту
|
||||
`${CLAUDE_PLUGIN_ROOT}/skills/review-pipeline/references/finding-contract.md`
|
||||
`${CLAUDE_PLUGIN_ROOT}/skills/review/references/finding-contract.md`
|
||||
(точный путь конвейер передаёт в задании).
|
||||
|
||||
**Ты помечен «держит машину».** Конвейер за это ставит тебя в цепочку с другими
|
||||
@@ -46,7 +46,7 @@ color: green
|
||||
`docs/research/` процессный документ, и прогон его не открывает; чужое число
|
||||
неизвестной свежести делало находку похожей на доказанную, ничего не доказывая.
|
||||
Почему именно так и какие ещё есть стыки —
|
||||
`${CLAUDE_PLUGIN_ROOT}/skills/review-pipeline/references/project-facts.md`, раздел
|
||||
`${CLAUDE_PLUGIN_ROOT}/skills/review/references/project-facts.md`, раздел
|
||||
«Сшивать обязаны проходы». Там же карта «что нужно проходу → где лежит».
|
||||
|
||||
Два обстоятельства почти всегда меняют цену отказов, и если документы их
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: review-rubric
|
||||
description: "Generative-проход ревью — сперва, НЕ ВИДЯ КОДА, порождает 8–12 проверяемых свойств, по которым сильный инженер судит узел такого назначения (парсер входного формата, HTTP-обработчик, репозиторий, воркер, клиент внешнего сервиса, CLI-команда, файловое хранилище), и только потом читает код и оценивает по этой рубрике. Достаёт слой, которого нет ни в одной конвенции. Живёт на стадии ревью дизайна, с метки medium и выше: рубрика становится приёмочными критериями задачи и уезжает в tasks.md. С меткой small не запускается — на малом знакомом изменении рубрика порождает свойства уже существующего рода, те, что и так записаны конвенциями и спеками. Только чтение."
|
||||
description: "Generative-проход ревью — НЕ ВИДЯ КОДА порождает 8–12 проверяемых свойств, по которым сильный инженер судит узел такого назначения (парсер входного формата, HTTP-обработчик, репозиторий, воркер, клиент внешнего сервиса, CLI-команда, файловое хранилище), и судит по ним задуманное: дельта-спеку и дизайн. Достаёт слой, которого нет ни в одной конвенции. Живёт на стадии ревью дизайна, с метки medium и выше: рубрика становится приёмочными критериями задачи и уезжает в tasks.md. Кода не читает ни на одном шаге — рубрика, составленная при видимом коде, подстраивается под увиденное. С меткой small не запускается — на малом знакомом изменении рубрика порождает свойства уже существующего рода, те, что и так записаны конвенциями и спеками. Только чтение."
|
||||
tools: Read, Grep, Glob, Bash
|
||||
model: opus
|
||||
color: yellow
|
||||
@@ -11,7 +11,7 @@ color: yellow
|
||||
**порождаешь сам** — и делаешь это до того, как увидишь код.
|
||||
|
||||
Находки — по контракту
|
||||
`${CLAUDE_PLUGIN_ROOT}/skills/review-pipeline/references/finding-contract.md`
|
||||
`${CLAUDE_PLUGIN_ROOT}/skills/review/references/finding-contract.md`
|
||||
(точный путь конвейер передаёт в задании). Русская проза, идентификаторы — в
|
||||
оригинале.
|
||||
|
||||
@@ -26,18 +26,16 @@ color: yellow
|
||||
сформулированное по прецеденту, сильнее любого общего.
|
||||
|
||||
Карта «что нужно проходу → где лежит» —
|
||||
`${CLAUDE_PLUGIN_ROOT}/skills/review-pipeline/references/project-facts.md`.
|
||||
`${CLAUDE_PLUGIN_ROOT}/skills/review/references/project-facts.md`.
|
||||
|
||||
**Документа нет — строка на каждый, отдельно.** Нет `docs/review.md`: «рода
|
||||
узлов и прецеденты неизвестны; требование „минимум три пункта специфичны для
|
||||
типа узла" выполнено по общей практике, а не по этому проекту». Нет инвариантов
|
||||
в `CLAUDE.md`: `critical` по основанию «нарушен инвариант проекта» в фазе 2 не
|
||||
присваивай и скажи об этом. Одной строкой за два документа не отделывайся —
|
||||
чинятся они разным.
|
||||
в `CLAUDE.md`: `critical` по основанию «нарушен инвариант проекта» не присваивай
|
||||
и скажи об этом. Одной строкой за два документа не отделывайся — чинятся они
|
||||
разным.
|
||||
|
||||
## Порядок фаз обязателен
|
||||
|
||||
### Фаза 1 — рубрика. Код читать ЗАПРЕЩЕНО
|
||||
## Рубрика. Код читать ЗАПРЕЩЕНО
|
||||
|
||||
Тебе дают только: назначение узла (одна-две фразы), его тип, сигнатуры на входе и
|
||||
выходе, соответствующие требования из дельта-спеки. **Не открывай файлы
|
||||
@@ -86,28 +84,29 @@ color: yellow
|
||||
Тот же вопрос на **готовом коде** задаёт эксплуатационный проход
|
||||
(вопрос 9); здесь он задаётся дизайну.
|
||||
|
||||
Выведи рубрику **до** любых находок. Она — часть результата, даже если код
|
||||
окажется идеальным.
|
||||
Выведи рубрику **до** любых находок. Она — часть результата, даже если
|
||||
задуманное окажется безупречным.
|
||||
|
||||
### Фаза 2 — оценка
|
||||
## По рубрике судится задуманное, а не код
|
||||
|
||||
Выполняется только если тебя позвали на готовый код (вне стадии ревью дизайна).
|
||||
Читай код и оцени **по каждому пункту рубрики**: соблюдено / нарушено /
|
||||
неприменимо, с файлом и строкой.
|
||||
Пройди рубрику против **дельта-спеки и дизайна**. Находка — там, где задуманное
|
||||
пункту прямо противоречит либо оставляет его неопределённым в месте, где
|
||||
определённость обязательна («что происходит при перекрытии тиков» не сказано ни
|
||||
в спеке, ни в дизайне). Остальные пункты уезжают приёмочными критериями в
|
||||
`tasks.md` change: там их и проверит приёмка.
|
||||
|
||||
**Новые критерии на этой фазе не добавляются.** Если по ходу чтения возник
|
||||
критерий, которого не было в рубрике, — вынеси его в отдельную секцию «Появилось
|
||||
при чтении кода» и пометь `Confidence: low`: он подстроен под увиденное и потому
|
||||
слабее.
|
||||
**Оценки кода у этого прохода нет, и это решение, а не пробел.** Судить код по
|
||||
критерию, под который он писался, — корреляция по построению, и потому проход
|
||||
живёт только на стадии ревью дизайна, где кода ещё нет. Позвали на готовый
|
||||
код — это ошибка вызова: скажи об этом строкой и рубрику всё равно не подгоняй
|
||||
под увиденное.
|
||||
|
||||
## Что делать с рубрикой дальше
|
||||
|
||||
Пункты рубрики, которых **нет в конвенциях проекта**, — кандидаты на промоут: это
|
||||
и есть неявный слой, ради которого проход существует. Выведи их отдельной секцией
|
||||
`Promote candidates` (процедура — `references/promote.md`).
|
||||
|
||||
На стадии ревью дизайна (кода ещё нет) фаза 2 не выполняется: рубрика уезжает в
|
||||
`tasks.md` change как приёмочные критерии.
|
||||
`Promote candidates` (процедура —
|
||||
`${CLAUDE_PLUGIN_ROOT}/skills/review/references/promote.md`).
|
||||
|
||||
## Чего этот проход принципиально не может поймать
|
||||
|
||||
@@ -122,22 +121,21 @@ color: yellow
|
||||
|
||||
## Формат вывода
|
||||
|
||||
1. `## Рубрика` — нумерованный список свойств (порождена до чтения кода).
|
||||
2. `## Оценка` — по каждому пункту: соблюдено/нарушено/неприменимо + файл:строка
|
||||
(только вне стадии ревью дизайна).
|
||||
3. Находки по контракту — только по нарушенным пунктам.
|
||||
4. `## Появилось при чтении кода` — если было.
|
||||
5. `## Promote candidates`.
|
||||
6. Обязательный блок:
|
||||
1. `## Рубрика` — нумерованный список свойств (порождена до чтения спеки).
|
||||
2. `## Разбор` — по каждому пункту: покрыт задуманным / противоречие /
|
||||
не определён / неприменим, со ссылкой на требование или раздел дизайна.
|
||||
3. Находки по контракту — только по пунктам с противоречием и неопределённостью.
|
||||
4. `## Promote candidates`.
|
||||
5. Обязательный блок:
|
||||
|
||||
```
|
||||
## Coverage of this pass
|
||||
- проверено: <какие пункты рубрики против каких файлов>
|
||||
- проверено: <какие пункты рубрики против каких требований и разделов дизайна>
|
||||
- не проверялось и почему: ...
|
||||
- принципиально недоступно этому проходу: рантайм, сверка со спекой, межмодульные связи
|
||||
- принципиально недоступно этому проходу: код, рантайм, сверка со спекой, межмодульные связи
|
||||
```
|
||||
|
||||
## Ограничения
|
||||
|
||||
Только чтение. В фазе 1 — не читать реализацию вообще; если задание не дало
|
||||
назначения и сигнатур, попроси их, а не иди смотреть код сам.
|
||||
Только чтение, и реализацию не читать вообще; если задание не дало назначения и
|
||||
сигнатур, попроси их, а не иди смотреть код сам.
|
||||
@@ -74,6 +74,15 @@ color: green
|
||||
| **`tasks.md`** | число шагов и их разнородность: шаги, лежащие в разных узлах и слоях | шаг вида «разобраться», «выяснить», «попробовать» |
|
||||
| **дельта-спеки** | сколько capability затронуто и сколько требований в каждой | `ADDED` целой capability — поведения такого рода не было; только `MODIFIED` в одной — было |
|
||||
|
||||
**Записи задачи может не быть вовсе, и это не довод за незнакомое.** Задача
|
||||
приходит текстом или из проекта без плагина задач — тогда раздела «Затрагивает»
|
||||
нет **по построению**, а не потому, что границы не назвали. Отличай:
|
||||
запись есть, а раздела в ней нет → незнакомое, как сказано в таблице; записи нет
|
||||
→ строка источника снимается, обе оси выводятся из остальных четырёх, и это
|
||||
называется в плане строкой «записи задачи нет, оси выведены по четырём
|
||||
источникам». Иначе всякая задача без плагина задач систематически едет в `large`
|
||||
за то, чего никто не терял.
|
||||
|
||||
**`design.md` информативен и своим отсутствием.** Его нет — либо задача
|
||||
тривиальна (тогда это подтверждает малое и знакомое), либо нетривиальную завели
|
||||
без разбора решения, и тогда «форму знали заранее» ничем не подтверждено: считай
|
||||
@@ -186,7 +195,7 @@ color: green
|
||||
**Ты меряешь изменение по двум независимым осям и называешь обе.** Метка — не
|
||||
ответ на один вопрос, а максимум по двум измерениям.
|
||||
|
||||
Ниже рабочая выжимка. Дом правила — скилл `av-dev-pipeline:review-pipeline`,
|
||||
Ниже рабочая выжимка. Дом правила — скилл `av-dev-code:review`,
|
||||
`references/review-levels.md`: там разобрано, почему оси именно эти, чем `small`
|
||||
дешевле `medium` и какие доли служат проверкой правила. Открывай его, когда
|
||||
метка **спорная или оспорена**; на обычной задаче хватает того, что здесь.
|
||||
@@ -333,7 +342,7 @@ security docs/security.md разбор basics
|
||||
operations docs/architecture.md, «Эксплуатация» разбор basics
|
||||
дома нет: docs/database.md отсутствует
|
||||
|
||||
процессные: docs/tasks/, docs/review.md, docs/adr/, docs/research/
|
||||
процессные: tasks/, docs/review.md, docs/adr/, docs/research/
|
||||
директивы: CLAUDE.md найден, AGENTS.md отсутствует
|
||||
```
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: review-specs
|
||||
description: "Сверка изменения с дельта-спеками в обе стороны — spec→code (каждое требование реализовано и подтверждено тестом) и, что важнее, code→spec (поведение, которое код имеет, а спека не заказывала: тихие ветки, самодеятельные дефолты, проглоченные ошибки, отброшенные поля, ретраи «на всякий случай»). Плюс границы спеки — что она не определяет и что пришлось домыслить. Работает в трёх режимах: дизайн/спеки ДО кода, код против спек ПОСЛЕ apply и стык после слияния нескольких задач, когда change уже заархивированы. Только чтение."
|
||||
description: "Сверка изменения с дельта-спеками в обе стороны — spec→code (каждое требование реализовано и подтверждено тестом) и, что важнее, code→spec (поведение, которое код имеет, а спека не заказывала: тихие ветки, самодеятельные дефолты, проглоченные ошибки, отброшенные поля, ретраи «на всякий случай»). Плюс границы спеки — что она не определяет и что пришлось домыслить. Работает в двух режимах: дизайн/спеки ДО кода и код против спек ПОСЛЕ apply. Только чтение."
|
||||
tools: Read, Grep, Glob, Bash
|
||||
model: opus
|
||||
color: yellow
|
||||
@@ -10,7 +10,7 @@ color: yellow
|
||||
Development на OpenSpec). Оптика — требования, а не стиль кода.
|
||||
|
||||
Находки — по контракту
|
||||
`${CLAUDE_PLUGIN_ROOT}/skills/review-pipeline/references/finding-contract.md`
|
||||
`${CLAUDE_PLUGIN_ROOT}/skills/review/references/finding-contract.md`
|
||||
(точный путь конвейер передаёт в задании). Русская проза; идентификаторы, пути и
|
||||
ключевые слова спек (`SHALL`, `GIVEN/WHEN/THEN`) — в оригинале. Читай реальные
|
||||
файлы перед выводом, ничего не выдумывай.
|
||||
@@ -49,7 +49,7 @@ Development на OpenSpec). Оптика — требования, а не ст
|
||||
|
||||
Пути спек жёсткие: актуальные — `openspec/specs/<capability>/spec.md`, дельты —
|
||||
`openspec/changes/<id>/specs/`. Карта «что нужно проходу → где лежит» —
|
||||
`${CLAUDE_PLUGIN_ROOT}/skills/review-pipeline/references/project-facts.md`.
|
||||
`${CLAUDE_PLUGIN_ROOT}/skills/review/references/project-facts.md`.
|
||||
|
||||
**Нет инвариантов в `CLAUDE.md`** — сверяй только спеку с кодом, `critical` по
|
||||
основанию «нарушен инвариант проекта» не присваивай и дай строку: «инвариантов в
|
||||
@@ -63,11 +63,9 @@ Development на OpenSpec). Оптика — требования, а не ст
|
||||
намерение, а спека нормирует. Расхождение между proposal и дельтой — само по себе
|
||||
находка.
|
||||
|
||||
**Исключение — режим 3 (ниже): живого change нет.** Тогда источник требований —
|
||||
**актуальные** `openspec/specs/<capability>/spec.md`, а дельты поднимаются из
|
||||
архива (`openspec/changes/archive/<id>/specs/`) как свидетельство о намерении
|
||||
каждой слитой задачи. Задание обязано назвать этот режим явно; не названо —
|
||||
работаешь по режиму 1 или 2 и говоришь в границах покрытия, что change не нашёл.
|
||||
**Живого change нет — ты не запускаешься.** Оба режима стоят на дельта-спеке; без
|
||||
неё сверять нечего, и это строка отказа, а не повод взять источником актуальные
|
||||
спеки: они описывают, что система делает вообще, а не что заказало это изменение.
|
||||
|
||||
Дополнительно поднимаешь **с метки `medium`**: `design.md` и `tasks.md`
|
||||
change, затронутые актуальные спеки. Инварианты из `CLAUDE.md` — при любой метке. Если тема ещё не перенесена в спеки и живёт только в
|
||||
@@ -144,27 +142,6 @@ change, затронутые актуальные спеки. Инвариант
|
||||
скажи об этом прямо, с последствием. Такая находка всегда `Действие: развилка`:
|
||||
менять спеку — решение человека.
|
||||
|
||||
## Режим 3 — стык после слияния нескольких задач
|
||||
|
||||
Зовётся финальной сверкой `task-batch`: несколько задач влиты в основную ветку,
|
||||
их change **уже заархивированы**, живой дельта-спеки не существует. Предмет —
|
||||
**только то, что появилось от слияния**, а не capability целиком заново: каждая
|
||||
задача уже проверена в своём worktree, и повторение даст те же находки дороже.
|
||||
|
||||
Ищешь ровно три вещи:
|
||||
|
||||
- **отменённое требование** — одна задача его выполнила, соседняя незаметно
|
||||
сняла; в актуальной спеке требование есть, в интегрированном коде его больше
|
||||
нет;
|
||||
- **два описания одного поведения** — два архивных change по-разному нормировали
|
||||
одно и то же, и актуальная спека собрала из них противоречие;
|
||||
- **осиротевшее поведение** — код, пришедший от слияния (разрешение конфликта,
|
||||
правка при rebase), которого не заказывал ни один из change.
|
||||
|
||||
База — интегрированный дифф основной ветки против точки, с которой батч начался.
|
||||
В границах покрытия скажи прямо: **capability целиком в этом режиме не
|
||||
сверялась**, проверялись стыки.
|
||||
|
||||
## Чего этот проход принципиально не может поймать
|
||||
|
||||
- Качество формы решения: код может точно соответствовать спеке и быть плохим.
|
||||
@@ -16,7 +16,7 @@ color: yellow
|
||||
Потолок в 7 пунктов защищает код, а не читателя.
|
||||
|
||||
Контракт находок и формат финального отчёта —
|
||||
`${CLAUDE_PLUGIN_ROOT}/skills/review-pipeline/references/finding-contract.md`
|
||||
`${CLAUDE_PLUGIN_ROOT}/skills/review/references/finding-contract.md`
|
||||
(точный путь конвейер передаёт в задании).
|
||||
|
||||
## Вход
|
||||
@@ -29,12 +29,10 @@ color: yellow
|
||||
и метка с обоснованием. Он твой главный инструмент сверки: ты единственный, кто
|
||||
видит и то, что размечено, и то, что пришло.
|
||||
|
||||
**Плана нет — ты не запускаешься.** Сверка размеченного с пришедшим — твоя
|
||||
единственная защита от молчащего пропуска, и без плана она не выполняется вовсе.
|
||||
Отчёт, собранный без неё, выглядит полным ровно настолько же, насколько и
|
||||
неполный. Исключение одно и объявленное: финальная сверка стыка в
|
||||
`av-dev-pipeline:task-batch` — там разметчика нет по построению, и план тебе
|
||||
собирает сам батч, коротким списком запущенного.
|
||||
**Плана нет — ты не запускаешься, и исключений нет.** Сверка размеченного с
|
||||
пришедшим — твоя единственная защита от молчащего пропуска, и без плана она не
|
||||
выполняется вовсе. Отчёт, собранный без неё, выглядит полным ровно настолько же,
|
||||
насколько и неполный.
|
||||
|
||||
Из документов проекта тебе нужны:
|
||||
|
||||
@@ -49,7 +47,7 @@ color: yellow
|
||||
целиком уезжают в границы покрытия и **не сливаются в один список**.
|
||||
|
||||
Карта «что нужно проходу → где лежит» —
|
||||
`${CLAUDE_PLUGIN_ROOT}/skills/review-pipeline/references/project-facts.md`.
|
||||
`${CLAUDE_PLUGIN_ROOT}/skills/review/references/project-facts.md`.
|
||||
|
||||
**Деградация поразрядная, и ты — тот, кто собирает её строки в один список,
|
||||
сохраняя каждую.** Свою часть
|
||||
@@ -209,7 +207,7 @@ severity:
|
||||
|
||||
1. **Решения проекта не сверялись.** `docs/adr.*` — процессный документ, прогон
|
||||
его не открывает. Расхождение изменения с записанным решением ловит сверка
|
||||
документации между спринтами, а не ревью.
|
||||
документации — скилл `av-dev-docs:healthcheck`, а не ревью.
|
||||
2. **Записанные наблюдения проекта не использовались.** `docs/research.*` — тоже
|
||||
процессный. Всякое число в находках снято проходом на этом прогоне; числа без
|
||||
приложенной команды замера в отчёте быть не должно.
|
||||
@@ -0,0 +1,167 @@
|
||||
---
|
||||
name: openspec
|
||||
description: "Завести и настроить OpenSpec в проекте — openspec init --tools claude, замена закомментированного примера в openspec/config.yaml на настройку канонической формы (язык, правила именования capability, придирки валидатора, адреса паспорта и CLAUDE.md), проверка формы своим скриптом openspec.py (имя файла, схема, незаменённый пример, адреса документов, ключи rules против артефактов схемы) и сверка слепка с живой версией инструмента. Использовать, когда в проекте нет каталога openspec/, когда config.yaml остался примером из коробки, когда заводят новый проект или переводят чужой и дошли до шага OpenSpec, а также когда конвейер отказался работать без источника требований. Каталог openspec нужен именно конвейеру: без него не работают ни opsx:propose, ни ревью дизайна, ни сверка требований."
|
||||
---
|
||||
|
||||
# OpenSpec в проекте
|
||||
|
||||
Каталог `openspec/` — **предпосылка конвейера**, а не канона документов. Без него
|
||||
не работают ни `opsx:propose`, ни ревью дизайна, ни `review-specs`: у требований
|
||||
не остаётся дома. Поэтому заводит и настраивает его этот плагин — тот, кто по
|
||||
OpenSpec и работает.
|
||||
|
||||
Канон документов о файле не высказывается вовсе: `docs.py` его не открывает и об
|
||||
его отсутствии молчит. Проект без конвейера живёт без OpenSpec законно, и
|
||||
проверять там нечего. За каноном остаётся одно — **единственный дом**: не
|
||||
пересказан ли в `context` документ, у которого есть свой файл. Это суждение, а не
|
||||
форма, и смотрит его агент.
|
||||
|
||||
## Два шага, и второй важнее первого
|
||||
|
||||
**1. Завести.**
|
||||
|
||||
```
|
||||
openspec init --tools claude
|
||||
```
|
||||
|
||||
Команда кладёт ещё `.claude/skills/openspec-*` и `.claude/commands/opsx/*` — это
|
||||
её нормальная работа, не трогай их.
|
||||
|
||||
**2. Заменить пример.** `openspec init` кладёт `config.yaml`, где `context` и
|
||||
`rules` — закомментированный пример на английском. **Файл из коробки хуже
|
||||
отсутствующего:** он есть, он валиден, имя правильное, — и читается как
|
||||
настроенный, работая как пустой. Узнаётся это по уже написанному предложению: на
|
||||
другом языке, с capability по имени пакета, без единого `SHALL`.
|
||||
|
||||
Пример **заменяется целиком** по образцу:
|
||||
[references/config-skeleton.md](references/config-skeleton.md).
|
||||
|
||||
## Что туда пишут, а что нет
|
||||
|
||||
**Это маршрутизатор, а не второй дом фактов.** Внутрь идёт ровно то, что нужно
|
||||
**в момент порождения артефакта** и чего в этот момент ещё никто не открыл: язык,
|
||||
правила именования capability, придирки валидатора и **адреса** документов
|
||||
проекта.
|
||||
|
||||
Сюда же — **требования к форме `proposal` и `design`**, на которых стоит чекпоинт
|
||||
скилла `av-dev-code:resolve`: объяснение человеку собирается из этих двух
|
||||
артефактов, и требование к ним обязано применяться в момент, когда их пишут, а не
|
||||
вспоминаться шагом позже. Образец их содержит.
|
||||
|
||||
Пересказ паспорта, инвариантов, конвенций и правил ревью сюда **не переносится**.
|
||||
Место для второго дома здесь самое частое: `context` читается при порождении
|
||||
каждого артефакта, туда удобно дописать «чтобы агент знал», и так заводятся копии
|
||||
инвариантов, состава гейта и правил выбора метки. Расходятся они молча, а
|
||||
замечают это в уже написанном предложении.
|
||||
|
||||
Разрез, по которому отличают одно от другого: **утверждение, которое можно
|
||||
опровергнуть, открыв другой файл проекта, — пересказ; строка, которая говорит,
|
||||
какой файл открыть, — ссылка.** Машина этот разрез не проверяет; его смотрит
|
||||
агент `doc-consistency` из плагина канона, когда тот подключён.
|
||||
|
||||
Два адреса обязательны — `docs/passport.md` и `CLAUDE.md`: предложение пишется до
|
||||
того, как кто-либо откроет `docs/`, и без них его пишут, не зная ни границы
|
||||
домена, ни инвариантов. Отсутствие адреса к **существующему** документу
|
||||
`openspec.py check` называет отказом; документа нет в проекте — нет и требования.
|
||||
|
||||
## Инструмент
|
||||
|
||||
```
|
||||
os="$CLAUDE_PLUGIN_ROOT/skills/openspec/scripts/openspec.py"
|
||||
|
||||
python3 $os check --dir <корень> # форма config.yaml в проекте
|
||||
python3 $os form # слепок формы против живого OpenSpec
|
||||
```
|
||||
|
||||
**Коды выхода — общий словарь скриптов av-dev:** 0 сошлось, 1 дрейф, 2 ошибка
|
||||
употребления, 3 окружение, 4 внутренний сбой. Ветвись на коде, а не на тексте.
|
||||
Различать 1 и 3 обязательно: «форма разошлась» — рабочая ситуация, «openspec не
|
||||
отвечает» — нерабочая.
|
||||
|
||||
`check` проверяет форму, и каждая проверка — про молчащий пробел, а не про вкус:
|
||||
каталог есть; имя именно `config.yaml` (`config.yml` OpenSpec не читает и об этом
|
||||
не сообщает); ключ `schema` называет ту схему, для которой форма описана;
|
||||
`context` и `rules.specs` не остались примером, **а `SHALL` назван именно внутри
|
||||
`rules.specs`** (в `context` он стоит и в образце, поэтому греп по файлу здесь
|
||||
ничего не значит); `context` называет паспорт и `CLAUDE.md`; ключи под `rules:` —
|
||||
имена артефактов схемы, а не свободные слова. Числа проверок здесь нет намеренно:
|
||||
оно протухает от каждой добавленной.
|
||||
|
||||
**Адреса требуются только к тем документам, которые в проекте есть.** Канон
|
||||
документов ставится отдельным плагином и может быть не подключён; требовать
|
||||
ссылку на несуществующий файл значит требовать битую ссылку. Нет
|
||||
`docs/passport.md` — проверка по нему идёт строкой «не проверялось», и там же
|
||||
сказано, что без канона конвейер работает вслепую.
|
||||
|
||||
### Форма сверяется с живым инструментом
|
||||
|
||||
Схема (`spec-driven`) и перечень артефактов (`proposal`, `specs`, `design`,
|
||||
`tasks`) — **состояние чужого инструмента**, а не наше решение. OpenSpec
|
||||
переименует артефакт: правила под прежним именем перестанут применяться, конфиг
|
||||
останется выглядеть написанным, и молчат при этом все три стороны.
|
||||
|
||||
Сторож — сравнение версий. `check` каждым прогоном спрашивает `openspec
|
||||
--version` (десятые доли секунды) и сравнивает `major.minor` с той версией, на
|
||||
которой форма сверялась; разошлось — **замечание**, не отказ, с именем команды.
|
||||
Патч-версия в сравнение не берётся намеренно: формы она не меняет, а нагоняй на
|
||||
каждый багфикс приучает пролистывать весь блок.
|
||||
|
||||
Перепроверяет `openspec.py form`: он спрашивает `openspec templates --json`, то
|
||||
есть перечень артефактов текущей схемы, и печатает, что разошлось с константами.
|
||||
Дорогой вызов вынесен из `check` сознательно — он стоит втрое дороже опроса
|
||||
версии, а ответ меняется только вместе с версией. **Чинится расхождение в
|
||||
плагине, а не в проекте:** константы скрипта, образец
|
||||
[references/config-skeleton.md](references/config-skeleton.md) и запись в журнал
|
||||
версий канона.
|
||||
|
||||
## Кто зовёт этот скилл
|
||||
|
||||
- `av-dev-docs:init` — шагом заведения нового проекта, до первого документа;
|
||||
- `av-dev-docs:canon` в режиме `adopt` — если на переводимом проекте каталога нет
|
||||
или `config.yaml` остался примером;
|
||||
- `av-dev-code:resolve` и `av-dev-code:review` — не вызовом по ходу, а отсылкой:
|
||||
OpenSpec у обоих жёсткая предпосылка, и на проекте без каталога оба посылают
|
||||
сюда вместо того, чтобы заводить его руками;
|
||||
- человек — когда конвейер отказался работать без источника требований.
|
||||
|
||||
**Копия.** Дом правила — `shared/plugin-boundary.md` в репозитории плагинов.
|
||||
Правится дом, а не этот файл.
|
||||
|
||||
<!-- копия: граница-плагинов из shared/plugin-boundary.md -->
|
||||
|
||||
Плагины `av-dev` ставятся порознь, и ни один не вправе считать, что сосед на
|
||||
месте.
|
||||
|
||||
**Чужой скилл зовётся полным именем** — `av-dev-docs:canon`, `av-dev-tasks:tasks`,
|
||||
`av-dev-code:review`. Короткое имя может разрешиться в устаревшую проектную копию
|
||||
из `.claude/skills/`, и подмены не будет видно ни в докладе, ни в поведении.
|
||||
|
||||
**Путь в дерево чужого плагина не пишется никогда.** `$CLAUDE_PLUGIN_ROOT` ведёт
|
||||
только в свой плагин; вычисленный от него путь к соседу либо не откроется, либо
|
||||
откроет чужую установку. Нужен чужой справочник — зови владеющий им скилл, он
|
||||
прочитает его сам.
|
||||
|
||||
**Вызов не разрешился — плагина в проекте нет.** Это исход, а не поломка: назови
|
||||
строкой доклада, чего теперь не делает никто, и продолжай работу. Молчать нельзя,
|
||||
пропуск неотличим от сделанного; выдумывать обходной путь нельзя тоже.
|
||||
|
||||
**Присутствие узнаётся вызовом или следом в проекте, но не объявлением.** Перечня
|
||||
установленных плагинов проект не ведёт — он разошёлся бы с действительностью
|
||||
молча. Что сосед здесь работал, видно по заведённому им файлу: `docs/.pm.json` —
|
||||
канон, `<каталог задач>/.tasks.json` — задачи, `openspec/config.yaml` — конвейер.
|
||||
|
||||
<!-- /копия: граница-плагинов -->
|
||||
|
||||
Здесь это значит: вызов не разрешился — плагина конвейера в проекте нет, и тогда
|
||||
OpenSpec заводит человек командой выше.
|
||||
|
||||
## Чего этот скилл не делает
|
||||
|
||||
- **Не пишет спеки и предложения.** Это `opsx:propose` и конвейер задачи.
|
||||
- **Не ведёт документы канона** — их дом плагин `av-dev-docs`, и адреса в
|
||||
`context` только на них ссылаются.
|
||||
- **Не чинит расхождение формы с версией OpenSpec в проекте.** Оно чинится в
|
||||
плагине: константы скрипта, образец здесь, запись в журнал версий канона.
|
||||
- **Не судит, ссылается `context` на документы или пересказывает их.** Машине
|
||||
этот разрез не виден; его смотрит агент `doc-consistency` из плагина канона.
|
||||
Плагина нет — эту проверку не делает никто, и так и скажи.
|
||||
@@ -0,0 +1,103 @@
|
||||
# Образец `openspec/config.yaml`
|
||||
|
||||
Каталог `openspec/` заводится командой — `openspec init --tools claude`, — и она
|
||||
кладёт `config.yaml` с закомментированным примером внутри. Пример **заменяется
|
||||
целиком**: нетронутый файл выглядит настроенным, а работает как пустой.
|
||||
|
||||
**Это маршрутизатор, а не второй дом фактов.** Сюда пишут ровно то, что нужно
|
||||
**в момент порождения артефакта** и чего в этот момент ещё никто не открыл:
|
||||
язык, правила именования capability, придирки валидатора и **адреса** документов
|
||||
канона. Пересказ паспорта, инвариантов, конвенций и правил ревью сюда не
|
||||
переносится: расходится он молча, а замечают это в уже написанном предложении.
|
||||
|
||||
```yaml
|
||||
schema: spec-driven
|
||||
|
||||
context: |
|
||||
Language: Russian
|
||||
Пиши на русском, но:
|
||||
- Структурные заголовки оставляй на английском:
|
||||
## ADDED/MODIFIED/REMOVED Requirements, ### Requirement:, #### Scenario:
|
||||
- Ключевые слова GIVEN/WHEN/THEN и RFC 2119 (SHALL/MUST/SHOULD) — на английском
|
||||
- Технические термины, пути и код — на английском
|
||||
|
||||
Имена capabilities:
|
||||
- Capability — это ПОВЕДЕНИЕ или домен системы, а не пакет кода (совпадение с
|
||||
именем пакета допустимо, но не критерий).
|
||||
- Существительное, понятное без знания кода: ingest, parsing, storage,
|
||||
read-api. НЕ store/httpapi — это реализация.
|
||||
- Гранулярность по принципу «требования меняются вместе». Дробить, когда в
|
||||
одной спеке смешиваются разные заботы. Переименовать дёшево (RENAMED
|
||||
Requirements) — не дроби преждевременно в маленьком проекте.
|
||||
|
||||
RFC 2119 — требование валидатора, не стиль:
|
||||
- Каждое ### Requirement ОБЯЗАНО содержать литерал SHALL или MUST, иначе
|
||||
`openspec validate` падает. Поэтому эти слова и WHEN/THEN не русифицируем.
|
||||
|
||||
Что это за проект — читай перед предложением, а не отсюда:
|
||||
- docs/passport.md — цель, её граница (чем проект НЕ является), потребители,
|
||||
типовые сценарии, референсы;
|
||||
- CLAUDE.md — инварианты с severity и семантика гейта;
|
||||
- docs/architecture.md — устройство; docs/security.md — периметр;
|
||||
docs/adr/ — почему решено так; docs/research/ — что уже измерено.
|
||||
Пересказа этих документов здесь нет намеренно: второй дом факта расходится с
|
||||
первым молча, и заметно это становится в предложении, которое уже написано.
|
||||
|
||||
Ревью: правило выбора метки и состав проходов здесь не пересказываем — их дом
|
||||
скилл av-dev-code:review, проектная настройка — docs/review.md.
|
||||
|
||||
Конвенции кода: механизированное проверяет гейт, прозой остаётся
|
||||
docs/conventions/. Ни состав шагов гейта, ни перечень конвенций здесь не
|
||||
пересказываем: и то и другое растёт по ходу задач.
|
||||
|
||||
Развилка или блокер — сперва prior art. Готовые решения смотрим в референсах
|
||||
паспорта, отвергаем — с названной причиной, и причина идёт в design.md этого
|
||||
же изменения.
|
||||
|
||||
rules:
|
||||
proposal:
|
||||
- Capabilities называй по поведению или домену системы, не по пакету кода
|
||||
- "Why и What Changes — языком домена из docs/passport.md: без SHALL, без имён модулей и функций там, где вещь называется по-русски"
|
||||
design:
|
||||
- "Назови рассмотренные варианты и причину отказа от каждого — из них потом пишется ADR"
|
||||
- "Решение объясняется через то, что человек увидит иначе, а не через устройство кода"
|
||||
specs:
|
||||
# Кавычки обязательны: без них YAML обрежет строку на первом '#'.
|
||||
- "Каждое ### Requirement обязано содержать SHALL или MUST (иначе валидация падает)"
|
||||
- "Сценарий — ровно #### (четыре решётки); три или список молча теряются"
|
||||
- "SHALL/MUST должно стоять в ПЕРВОМ абзаце требования: валидатор смотрит только его"
|
||||
- "Заголовки и WHEN/THEN/GIVEN — на английском, остальной текст на русском"
|
||||
tasks:
|
||||
- "Критерии приёмки задачи — отдельным блоком и дословно: файл задачи закрытие удалит, критерии обязаны его пережить"
|
||||
- "Рубрика ревью дизайна, если оно её дало, идёт в тот же блок: приёмка судится по одному списку, а не по двум"
|
||||
- "Шаг плана формулируется проверяемо — по нему видно «сделано / не сделано» без суждения"
|
||||
```
|
||||
|
||||
**Четыре правила для `specs` сняты отказами валидатора, а не выведены из
|
||||
документации** — потому и записаны дословно: без них каждое второе предложение
|
||||
узнаёт их падением `openspec validate --strict`.
|
||||
|
||||
**Правила для `proposal` и `design` держат чекпоинт скилла
|
||||
`av-dev-code:resolve`.** Там работа останавливается и человеку объясняют, в
|
||||
чём проблема и как её решают, — а объяснение **собирается из этих двух
|
||||
артефактов**, а не сочиняется заново: третий пересказ одного и того же разошёлся
|
||||
бы с обоими. Требование поэтому стоит здесь, в момент порождения артефакта, а не
|
||||
в скилле, который спохватится позже. `design.md` при этом ещё и **сырьё для
|
||||
ADR** — отвергнутый вариант с названной причиной и есть половина будущей записи.
|
||||
|
||||
**Правила для `tasks` держит тот же скилл, и по той же причине — момент
|
||||
порождения.** `tasks.md` — единственное, что переживает задачу: файл задачи
|
||||
закрытие удаляет, а приёмка потом судится по критериям, которые в него
|
||||
скопированы. Туда же ложится рубрика ревью дизайна, если оно её дало. Записанное
|
||||
в момент порождения не приходится вспоминать шагом позже, когда артефакт уже
|
||||
написан. Блок `context` проект
|
||||
дополняет своим (стек, разведка, особенности домена), но **адреса паспорта и
|
||||
`CLAUDE.md` обязательны** — отсутствие адреса к существующему документу
|
||||
`openspec.py check` называет отказом.
|
||||
|
||||
**Ключи под `rules:` — имена артефактов схемы**, а не свободные слова:
|
||||
`proposal`, `specs`, `design`, `tasks`. Правило под чужим именем не применяется
|
||||
и об этом не сообщает, поэтому `rules.spec` вместо `rules.specs` даёт конфиг,
|
||||
выглядящий написанным и не работающий; `openspec.py check` такой ключ называет.
|
||||
Перечень артефактов задаёт OpenSpec, а не мы, — за его актуальностью следит
|
||||
`openspec.py form`.
|
||||
@@ -0,0 +1,404 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Форма `openspec/config.yaml`: проверка проекта и сверка слепка с инструментом.
|
||||
|
||||
Каталог `openspec/` — предпосылка **конвейера**, а не канона документов: без него
|
||||
не работают ни `opsx:propose`, ни ревью дизайна, ни сверка требований. Поэтому и
|
||||
проверка формы живёт здесь, рядом со скиллом, который каталог заводит. Раньше она
|
||||
жила в `docs.py` плагина канона, и у файла было два владельца: один заводит,
|
||||
другой проверяет.
|
||||
|
||||
Проверяется то, что **молчит при поломке**. Файл из коробки хуже отсутствующего:
|
||||
`openspec init` кладёт `config.yaml`, где `context` и `rules` — закомментированный
|
||||
пример на английском. Он есть, он валиден, имя правильное — и читается как
|
||||
настроенный, работая как пустой. Узнаётся это по уже написанному предложению.
|
||||
|
||||
Разбираем текстом, а не YAML-парсером: у скриптов ноль внешних зависимостей, а
|
||||
PyYAML в стандартной библиотеке нет. Всё проверяемое различимо построчно,
|
||||
комментарии отброшены, ключи верхнего уровня стоят в первой колонке.
|
||||
|
||||
Коды выхода — общий словарь скриптов av-dev:
|
||||
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
|
||||
from typing import NoReturn
|
||||
|
||||
OK, DRIFT, USAGE, ENV, INTERNAL = 0, 1, 2, 3, 4
|
||||
|
||||
# Команда заведения. Названа поимённо потому, что её печатает отказ, а отказ без
|
||||
# команды заставляет искать её в другом месте.
|
||||
OPENSPEC_INIT = "openspec init --tools claude"
|
||||
|
||||
# Адреса, которые обязан назвать блок context. Не пересказ документов, а именно
|
||||
# ссылки: предложение пишется до того, как кто-либо откроет docs/, и без этих
|
||||
# двух строк его пишут, не зная ни границы домена, ни инвариантов. Список
|
||||
# короткий намеренно — длинный превращает context во второй дом фактов.
|
||||
#
|
||||
# Третий элемент — путь, по которому проверяется, есть ли документ в проекте
|
||||
# вообще. Канон документов ставится отдельным плагином и может быть не подключён;
|
||||
# требовать ссылку на файл, которого нет, значит требовать битую ссылку.
|
||||
OPENSPEC_POINTERS = [
|
||||
("passport", "docs/passport.md",
|
||||
"граница домена и «чем НЕ является» останутся непрочитанными"),
|
||||
("CLAUDE.md", "CLAUDE.md",
|
||||
"инварианты и семантика гейта останутся непрочитанными"),
|
||||
]
|
||||
|
||||
# --- Слепок чужого инструмента ----------------------------------------------
|
||||
#
|
||||
# Схема, перечень артефактов и версия, на которой это проверено, живут в OpenSpec
|
||||
# и меняются без нашего участия; здесь они записаны, чтобы проверка шла без
|
||||
# запуска node на каждом прогоне.
|
||||
#
|
||||
# Слепок стареет, и потому есть кто это замечает: `check` сравнивает major.minor
|
||||
# установленного OpenSpec с OPENSPEC_CHECKED и, если они разошлись, говорит
|
||||
# замечанием «форма не перепроверена». Перепроверяет команда `form` — она
|
||||
# спрашивает сам инструмент и печатает, что разошлось. Патч-версия сравнением
|
||||
# намеренно не берётся: форма конфига в ней не меняется, а замечание на каждый
|
||||
# багфикс приучило бы пролистывать весь блок.
|
||||
OPENSPEC_CHECKED = "1.5"
|
||||
OPENSPEC_SCHEMA = "spec-driven"
|
||||
|
||||
# Артефакты схемы. Ключ `rules:` адресуется артефакту, и адресованный
|
||||
# несуществующему **молча не действует** — ровно тот класс, ради которого вся
|
||||
# проверка и заведена.
|
||||
OPENSPEC_ARTIFACTS = ("proposal", "specs", "design", "tasks")
|
||||
|
||||
|
||||
@dataclass
|
||||
class Report:
|
||||
errors: list[str] = field(default_factory=list)
|
||||
notes: 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 skip(self, msg: str) -> None:
|
||||
self.skipped.append(msg)
|
||||
|
||||
|
||||
def fail(code: int, msg: str) -> NoReturn:
|
||||
print(msg, file=sys.stderr)
|
||||
sys.exit(code)
|
||||
|
||||
|
||||
def openspec_cli(args: list[str]) -> str | None:
|
||||
"""Спросить сам инструмент. None — его нет или он не ответил."""
|
||||
try:
|
||||
out = subprocess.run(
|
||||
["openspec", *args], capture_output=True, text=True, timeout=30
|
||||
)
|
||||
except (FileNotFoundError, OSError, subprocess.SubprocessError):
|
||||
return None
|
||||
return out.stdout.strip() if out.returncode == 0 else None
|
||||
|
||||
|
||||
def rules_keys(live: str) -> list[str]:
|
||||
"""Имена артефактов, которым адресованы правила, — и только они.
|
||||
|
||||
Идём от строки `rules:` до следующего ключа нулевой колонки, а не ищем
|
||||
отступ по всему файлу: блок `context: |` — литеральный скаляр, внутри него
|
||||
строки вида «Language: Russian» и «av-dev-code:review» выглядят
|
||||
ключами и дали бы находку на ровном месте. Проверено на живом конфиге,
|
||||
который так и падал.
|
||||
"""
|
||||
out: list[str] = []
|
||||
inside = False
|
||||
for line in live.splitlines():
|
||||
if not line.strip():
|
||||
continue
|
||||
if not line[0].isspace():
|
||||
inside = line.startswith("rules:")
|
||||
continue
|
||||
if not inside:
|
||||
continue
|
||||
m = re.fullmatch(r" ([A-Za-z_-]+):\s*", line)
|
||||
if m:
|
||||
out.append(m.group(1))
|
||||
return out
|
||||
|
||||
|
||||
def rules_block(live: str, name: str) -> str:
|
||||
"""Строки правил, адресованных одному артефакту.
|
||||
|
||||
Обход тот же, что у `rules_keys`, и по той же причине: искать по всему файлу
|
||||
нельзя. Литеральный скаляр `context` называет `SHALL` уже в образце, поэтому
|
||||
проверка «правила называют SHALL» грепом по файлу проходила при **пустом**
|
||||
`rules.specs` — то есть молчала ровно в том случае, ради которого написана.
|
||||
"""
|
||||
out: list[str] = []
|
||||
in_rules = False
|
||||
in_name = False
|
||||
for line in live.splitlines():
|
||||
if not line.strip():
|
||||
continue
|
||||
if not line[0].isspace():
|
||||
in_rules = line.startswith("rules:")
|
||||
in_name = False
|
||||
continue
|
||||
if not in_rules:
|
||||
continue
|
||||
m = re.fullmatch(r" ([A-Za-z_-]+):\s*", line)
|
||||
if m:
|
||||
in_name = m.group(1) == name
|
||||
continue
|
||||
if in_name:
|
||||
out.append(line)
|
||||
return "\n".join(out)
|
||||
|
||||
|
||||
def check_form(root: Path, rep: Report) -> None:
|
||||
"""Настройка заведена и не осталась примером из коробки."""
|
||||
os_dir = root / "openspec"
|
||||
if not os_dir.is_dir():
|
||||
# Здесь это отказ, а не «неприменимо»: скрипт принадлежит конвейеру, а
|
||||
# конвейер без OpenSpec не работает вовсе. Тот же вопрос со стороны
|
||||
# канона документов звучит иначе, и `docs.py` отвечает на него молчанием.
|
||||
rep.error(
|
||||
"нет openspec/ — там дом темы requirements (openspec/specs/) и "
|
||||
f"настройка генерации артефактов; заводится `{OPENSPEC_INIT}`"
|
||||
)
|
||||
return
|
||||
|
||||
if (os_dir / "config.yml").is_file():
|
||||
rep.error(
|
||||
"openspec/config.yml — читается только config.yaml, и этот файл "
|
||||
"останется незамеченным: настройка будет пустой, а выглядеть будет "
|
||||
"заполненной"
|
||||
)
|
||||
|
||||
path = os_dir / "config.yaml"
|
||||
if not path.is_file():
|
||||
rep.error(
|
||||
"нет openspec/config.yaml — язык, правила именования capability и "
|
||||
"придирки валидатора будут заново угадываться на каждом предложении"
|
||||
)
|
||||
return
|
||||
|
||||
text = path.read_text(encoding="utf-8")
|
||||
live = "\n".join(
|
||||
line for line in text.splitlines() if not line.lstrip().startswith("#")
|
||||
)
|
||||
keys = set(re.findall(r"(?m)^([A-Za-z_]+):", live))
|
||||
|
||||
schema = re.search(r"(?m)^schema:\s*(\S+)", live)
|
||||
if schema is None:
|
||||
rep.error(
|
||||
f"в openspec/config.yaml нет ключа schema — ожидается {OPENSPEC_SCHEMA}"
|
||||
)
|
||||
elif schema.group(1) != OPENSPEC_SCHEMA:
|
||||
rep.error(
|
||||
f"schema в openspec/config.yaml — {schema.group(1)}, а форма описана "
|
||||
f"для {OPENSPEC_SCHEMA}"
|
||||
)
|
||||
|
||||
if "context" not in keys:
|
||||
rep.error(
|
||||
"в openspec/config.yaml нет ключа context: файл остался примером из "
|
||||
"коробки — предложение пишется без языка, правил именования "
|
||||
"capability и адресов документов проекта"
|
||||
)
|
||||
else:
|
||||
for pointer, where, why in OPENSPEC_POINTERS:
|
||||
if not (root / where).exists():
|
||||
rep.skip(
|
||||
f"{where} в проекте нет — ссылка на него в context не "
|
||||
f"требуется. Документы канона ведёт отдельный плагин "
|
||||
f"(av-dev-docs), и без него конвейер работает вслепую"
|
||||
)
|
||||
continue
|
||||
if pointer not in live:
|
||||
rep.error(f"openspec/config.yaml не называет {pointer} — {why}")
|
||||
|
||||
if "rules" not in keys or "specs" not in rules_keys(live):
|
||||
rep.error(
|
||||
"в openspec/config.yaml нет rules.specs — придирки валидатора "
|
||||
"нигде не записаны, и каждое предложение узнаёт их отказом"
|
||||
)
|
||||
elif "SHALL" not in rules_block(live, "specs"):
|
||||
rep.error(
|
||||
"rules.specs в openspec/config.yaml не называет SHALL — "
|
||||
"требование без этого литерала валидатор отвергает, а правило "
|
||||
"проекта об этом молчит"
|
||||
)
|
||||
|
||||
# Ключ под rules: — имя артефакта схемы. Опечатка или устаревшее имя не
|
||||
# ломает ничего видимого: правила просто не применяются, а конфиг выглядит
|
||||
# написанным.
|
||||
for name in rules_keys(live):
|
||||
if name not in OPENSPEC_ARTIFACTS:
|
||||
rep.error(
|
||||
f"rules.{name} в openspec/config.yaml — такого артефакта у схемы "
|
||||
f"{OPENSPEC_SCHEMA} нет ({', '.join(OPENSPEC_ARTIFACTS)}): правила "
|
||||
f"под ним не применяются и молчат об этом"
|
||||
)
|
||||
|
||||
|
||||
def check_fresh(rep: Report) -> None:
|
||||
"""Не устарел ли слепок формы.
|
||||
|
||||
Стоит один запуск `openspec --version` — десятые доли секунды. Перечень
|
||||
артефактов и имя схемы отсюда не спрашиваются намеренно: они стоят втрое
|
||||
дороже, а меняются только вместе с версией, и потому за ними ходит команда
|
||||
`form`, а эта проверка говорит, когда её звать.
|
||||
"""
|
||||
got = openspec_cli(["--version"])
|
||||
if got is None:
|
||||
rep.skip(
|
||||
"openspec не отвечает (нет на PATH?) — актуальность формы "
|
||||
"config.yaml не проверялась"
|
||||
)
|
||||
return
|
||||
installed = ".".join(got.split(".")[:2])
|
||||
if installed != OPENSPEC_CHECKED:
|
||||
rep.note(
|
||||
f"форма openspec/config.yaml сверена с OpenSpec {OPENSPEC_CHECKED}, "
|
||||
f"установлен {got}: перепроверить — `openspec.py form`. Пока не "
|
||||
f"перепроверено, проверки формы судят по прежней схеме"
|
||||
)
|
||||
|
||||
|
||||
def report(rep: Report) -> int:
|
||||
for msg in rep.errors:
|
||||
print(f"ДРЕЙФ {msg}")
|
||||
for msg in rep.notes:
|
||||
print(f"ЗАМЕЧАНИЕ {msg}")
|
||||
if rep.skipped:
|
||||
print("\nНЕ ПРОВЕРЯЛОСЬ:")
|
||||
for msg in rep.skipped:
|
||||
print(f" {msg}")
|
||||
|
||||
print(
|
||||
"\nМашина проверила форму: имя файла, схему, незаменённый пример, адреса\n"
|
||||
"документов проекта и ключи rules против артефактов схемы. Чего она не\n"
|
||||
"видит — **пересказ вместо ссылки**: утверждение, которое можно\n"
|
||||
"опровергнуть, открыв другой файл проекта, от строки «открой такой-то\n"
|
||||
"файл» она не отличает. Это суждение агента `doc-consistency` из плагина\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}")
|
||||
rep = Report()
|
||||
check_form(root, rep)
|
||||
check_fresh(rep)
|
||||
return report(rep)
|
||||
|
||||
|
||||
def cmd_form(args: argparse.Namespace) -> int:
|
||||
"""Перепроверить слепок формы по живому OpenSpec.
|
||||
|
||||
Ничего не правит и не трогает проект: спрашивает инструмент и печатает, что
|
||||
разошлось с константами скрипта. Чинит человек — правкой констант, образца в
|
||||
references/config-skeleton.md и записью в журнал версий канона, если форма
|
||||
действительно поменялась.
|
||||
"""
|
||||
version = openspec_cli(["--version"])
|
||||
if version is None:
|
||||
fail(
|
||||
ENV,
|
||||
"openspec не отвечает: поставь его или проверь PATH — "
|
||||
"перепроверять форму нечем",
|
||||
)
|
||||
raw = openspec_cli(["templates", "--json"])
|
||||
if raw is None:
|
||||
fail(ENV, "`openspec templates --json` не отработал — схему не спросить")
|
||||
try:
|
||||
artifacts = tuple(json.loads(raw))
|
||||
except json.JSONDecodeError as exc:
|
||||
fail(ENV, f"`openspec templates --json` отдал неразбираемое: {exc}")
|
||||
|
||||
print(f"OpenSpec установлен: {version}")
|
||||
print(f"форма сверена с: {OPENSPEC_CHECKED}")
|
||||
print(f"артефакты схемы: {', '.join(artifacts)}")
|
||||
print(f"записано в скрипте: {', '.join(OPENSPEC_ARTIFACTS)}")
|
||||
|
||||
diffs: list[str] = []
|
||||
if ".".join(version.split(".")[:2]) != OPENSPEC_CHECKED:
|
||||
diffs.append(
|
||||
f"версия: поднять OPENSPEC_CHECKED до "
|
||||
f"{'.'.join(version.split('.')[:2])} — но только после того, как "
|
||||
f"остальные строки этого отчёта сойдутся"
|
||||
)
|
||||
for name in artifacts:
|
||||
if name not in OPENSPEC_ARTIFACTS:
|
||||
diffs.append(
|
||||
f"новый артефакт {name}: решить, нужны ли ему правила в rules, "
|
||||
f"и добавить имя в OPENSPEC_ARTIFACTS"
|
||||
)
|
||||
for name in OPENSPEC_ARTIFACTS:
|
||||
if name not in artifacts:
|
||||
diffs.append(
|
||||
f"артефакта {name} у схемы больше нет: правила под ним в конфигах "
|
||||
f"проектов молчат — убрать из OPENSPEC_ARTIFACTS, из образца и "
|
||||
f"записать в журнал версий канона"
|
||||
)
|
||||
|
||||
print()
|
||||
if not diffs:
|
||||
print("Слепок сходится. Осталось глазами: не изменились ли придирки")
|
||||
print("валидатора — их скрипт проверить не может, они проявляются только")
|
||||
print("отказом `openspec validate --strict` на живой спеке.")
|
||||
return OK
|
||||
print("Разошлось:")
|
||||
for line in diffs:
|
||||
print(f" - {line}")
|
||||
print()
|
||||
print("Правится в трёх местах сразу: константы этого скрипта, образец")
|
||||
print("`references/config-skeleton.md` и запись в журнал версий канона —")
|
||||
print("иначе проекты останутся на прежней форме молча.")
|
||||
return DRIFT
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="openspec.py",
|
||||
description="форма openspec/config.yaml: проверка проекта и сверка слепка",
|
||||
)
|
||||
sub = parser.add_subparsers(dest="cmd", required=True)
|
||||
|
||||
p_check = sub.add_parser("check", help="форма config.yaml в проекте")
|
||||
p_check.add_argument("--dir", default=".", help="корень проекта")
|
||||
p_check.set_defaults(func=cmd_check)
|
||||
|
||||
p_form = sub.add_parser(
|
||||
"form", help="перепроверить слепок формы по живому OpenSpec"
|
||||
)
|
||||
p_form.set_defaults(func=cmd_form)
|
||||
|
||||
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,655 @@
|
||||
---
|
||||
name: resolve
|
||||
description: "Решить одну задачу от постановки до закрытия. На входе путь к файлу задачи, её слаг или просто текст. Обычная задача идёт циклом Spec Driven Development: opsx propose → разметка → ревью дизайна → чекпоинт с объяснением человеческим языком → opsx apply → ревью кода → archive → синк документации → коммит → закрытие. Исследовательская (тип research, сырая идея, мутная постановка) начинается раньше: opsx explore и чекпоинт вариантов — два-четыре способа решить, с ценой каждого и рекомендацией; выбор оседает по адресу, который назвала сама задача. Между чекпоинтами работа идёт без согласований. Использовать, когда просят взять, сделать или решить задачу, довести идею до реализации, разобраться с записью из беклога."
|
||||
---
|
||||
|
||||
# Решение одной задачи
|
||||
|
||||
Проводит **одну** задачу от постановки до закрытия. Между плановыми
|
||||
остановками — без согласований: механику не обсуждаем, делаем.
|
||||
|
||||
Тонкая обёртка над каноническими скиллами `opsx:explore` / `opsx:propose` /
|
||||
`opsx:apply` / `opsx:archive` — зови их через Skill, не переизобретай их шаги.
|
||||
Ревью — скилл `av-dev-code:review`; он же держит правило выбора
|
||||
метки, а называет её агент `review-scope` — один раз на задачу, для обеих стадий
|
||||
ревью.
|
||||
|
||||
## Предпосылки
|
||||
|
||||
- **OpenSpec и скиллы `opsx:*` — жёсткая предпосылка, а не опция.** На них стоят
|
||||
шаги 2, 6 и 8, шаг Р2 разведки, проход `review-specs` и ревью дизайна (они
|
||||
завязаны на `openspec/changes/<id>/specs/*/spec.md` и на
|
||||
`openspec validate --strict`). **Проект без OpenSpec этим скиллом не ведётся** —
|
||||
подключай OpenSpec, а не вырождай цикл; почему ветка деградации здесь не
|
||||
пишется, сказано в `av-dev-code:review`, раздел «Предпосылки», и дом у этого
|
||||
довода там. Заводить руками не надо: каталог и настройку в `config.yaml`
|
||||
делает скилл `av-dev-code:openspec`.
|
||||
- **Проектные копии этих скиллов и агентов удаляются при установке плагина**
|
||||
(`.claude/skills/` — голые имена `resolve`, `review`, а у проектов прошлого
|
||||
поколения ещё `task-pipeline`, `review-pipeline`, `task-batch`, и с префиксом
|
||||
проекта: `<проект>-task-pipeline`, `<проект>-review-pipeline`;
|
||||
`.claude/agents/<проект>-review-*.md`). Две копии одного скилла расходятся, и
|
||||
побеждает та, что короче названа.
|
||||
|
||||
### Обращение к соседним плагинам
|
||||
|
||||
**Копия.** Дом — `shared/plugin-boundary.md` в репозитории плагинов: правило
|
||||
общее для всех, кто зовёт чужое, и ни один плагин им не владеет. Правится дом, а
|
||||
не этот файл.
|
||||
|
||||
<!-- копия: граница-плагинов из shared/plugin-boundary.md -->
|
||||
|
||||
Плагины `av-dev` ставятся порознь, и ни один не вправе считать, что сосед на
|
||||
месте.
|
||||
|
||||
**Чужой скилл зовётся полным именем** — `av-dev-docs:canon`, `av-dev-tasks:tasks`,
|
||||
`av-dev-code:review`. Короткое имя может разрешиться в устаревшую проектную копию
|
||||
из `.claude/skills/`, и подмены не будет видно ни в докладе, ни в поведении.
|
||||
|
||||
**Путь в дерево чужого плагина не пишется никогда.** `$CLAUDE_PLUGIN_ROOT` ведёт
|
||||
только в свой плагин; вычисленный от него путь к соседу либо не откроется, либо
|
||||
откроет чужую установку. Нужен чужой справочник — зови владеющий им скилл, он
|
||||
прочитает его сам.
|
||||
|
||||
**Вызов не разрешился — плагина в проекте нет.** Это исход, а не поломка: назови
|
||||
строкой доклада, чего теперь не делает никто, и продолжай работу. Молчать нельзя,
|
||||
пропуск неотличим от сделанного; выдумывать обходной путь нельзя тоже.
|
||||
|
||||
**Присутствие узнаётся вызовом или следом в проекте, но не объявлением.** Перечня
|
||||
установленных плагинов проект не ведёт — он разошёлся бы с действительностью
|
||||
молча. Что сосед здесь работал, видно по заведённому им файлу: `docs/.pm.json` —
|
||||
канон, `<каталог задач>/.tasks.json` — задачи, `openspec/config.yaml` — конвейер.
|
||||
|
||||
<!-- /копия: граница-плагинов -->
|
||||
|
||||
Скилл зовёт `av-dev-code:review`, `av-dev-docs:docs` и
|
||||
`av-dev-tasks:tasks`. Чем оборачивается отсутствие каждого — на самих шагах и в
|
||||
разделе «Границы».
|
||||
|
||||
Перед стартом прочитай `CLAUDE.md` проекта и то, на что он ссылается, если ещё
|
||||
не в контексте. Проектные факты, нужные ревью — инварианты, семантика гейта,
|
||||
объёмы, модель угроз, прецеденты, — живут в **документах канона** `av-dev-docs`;
|
||||
карта «что где» — `references/project-facts.md` конвейера ревью.
|
||||
|
||||
**Документов канона нет — проект к нему не приведён.** Скажи это строкой и
|
||||
предложи скилл `av-dev-docs:canon`: одна операция на проект против поразрядной
|
||||
деградации на каждой задаче. Работу при этом не останавливай.
|
||||
|
||||
## Вход
|
||||
|
||||
Задача задаётся **путём к файлу, именем файла, слагом или просто текстом**.
|
||||
Ничего из этого не задано — попроси у вызывающего и остановись; сам в беклог не
|
||||
лезь и приоритеты не интерпретируй: что делать дальше, решает не этот скилл.
|
||||
|
||||
**Запись из каталога сперва проверяется на готовность, и проверяет её машина.**
|
||||
Вызови Skill `av-dev-tasks:tasks` и попроси прогнать `ready <слаг>`: он смотрит
|
||||
тип, цель у `feature`, пустой ли раздел вопросов и собраны ли разделы схемы
|
||||
типа. Судить это глазами нельзя — ровно тот случай, где машина дешевле и точнее,
|
||||
а цена ошибки отложенная: недостающие критерии приёмки обнаружатся на приёмке,
|
||||
когда сверять уже не с чем.
|
||||
|
||||
**`ready` отказал — это исход, а не препятствие для тебя.** Скажи, чего не
|
||||
хватает, и остановись: дописывать чужую запись за автора не твоя работа, а у
|
||||
сырья (`research` без раздела «Вопрос») и дописывать нечего — там сперва нужен
|
||||
вопрос. Исход — «не доведена», с названной причиной.
|
||||
|
||||
Плагина `av-dev-tasks` в проекте нет или задача пришла текстом — прогонять
|
||||
нечего. Тогда прочитай постановку сам и скажи строкой, что готовность машиной не
|
||||
проверялась; работу при этом не останавливай.
|
||||
|
||||
## Две ветки
|
||||
|
||||
Развилка одна и стоит на входе:
|
||||
|
||||
- **обычная задача** — что делать, понятно; спорно только как. Идёт с шага 1;
|
||||
- **исследовательская** — тип `research`, сырая идея, новое и незнакомое, мутная
|
||||
постановка. Идёт с шага Р1, и там её ждёт **свой** чекпоинт: варианты решения
|
||||
обсуждаются **до** того, как написано первое требование.
|
||||
|
||||
Признак не в объёме работы, а в том, **есть ли у задачи один очевидный способ
|
||||
решения**. Его нет — обсуждать варианты после `propose` поздно: предложение уже
|
||||
воплотило один из них, и разговор пойдёт не о выборе, а о переделке.
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
in["вход: файл, слаг или текст"]
|
||||
fork{"есть очевидный<br/>способ решения?"}
|
||||
r1["Р1. понять вопрос<br/>сырьё без «Вопроса» — отказ"]
|
||||
r2["Р2. opsx:explore — груминг"]
|
||||
r3(["Р3. ЧЕКПОИНТ: варианты<br/>2–4 способа, цена каждого,<br/>рекомендация"])
|
||||
rout["исход без кода:<br/>ответ записан / отказ / родились задачи"]
|
||||
s1["1. прочитать задачу<br/>критерии приёмки выписать сразу"]
|
||||
s2["2. opsx:propose — change, дельта-спеки, tasks.md"]
|
||||
s3["3. разметка — review-scope:<br/>размер, сложность, метка, план тем"]
|
||||
s4["4. ревью дизайна, состав по метке<br/>+ отработка замечаний"]
|
||||
s5(["5. ЧЕКПОИНТ: объяснение<br/>в чём проблема, как решаем,<br/>чем рискуем"])
|
||||
s6["6. opsx:apply — код, гейт,<br/>поведенческая верификация"]
|
||||
s7["7. ревью кода, та же метка<br/>+ отработка замечаний"]
|
||||
s8["8. opsx:archive"]
|
||||
s9["9. синк документации — av-dev-docs:docs"]
|
||||
s10["10. коммит работы — av-dev-git:commit"]
|
||||
s11["11. закрыть задачу — av-dev-tasks:tasks,<br/>вторым коммитом учёта"]
|
||||
|
||||
in --> fork
|
||||
fork -->|"да"| s1
|
||||
fork -->|"нет"| r1
|
||||
r1 --> r2 --> r3
|
||||
r3 -->|"выбран способ"| s1
|
||||
r3 -.->|"кода не будет"| rout
|
||||
s1 --> s2 --> s3 --> s4 --> s5 --> s6 --> s7 --> s8 --> s9 --> s10 --> s11
|
||||
s3 -.->|"план задачи: та же метка"| s7
|
||||
s5 -.->|"скорректировать:<br/>меняются дельта-спеки"| s3
|
||||
s7 -.->|"находка отменяет дизайн:<br/>меняются дельта-спеки"| s3
|
||||
```
|
||||
|
||||
Схема — **сводка**: содержание каждого шага в его разделе ниже, и при
|
||||
расхождении прав текст.
|
||||
|
||||
## Автономность и два плановых стопа
|
||||
|
||||
**Между чекпоинтами умолчание прежнее — делать, а не спрашивать.** Чекпоинты не
|
||||
отменяют автономность, они дают развилкам плановое место, куда копиться.
|
||||
|
||||
Разрез простой:
|
||||
|
||||
- развилка найдена **до** ближайшего чекпоинта — она его и ждёт. Не спрашивай
|
||||
отдельно: чекпоинт рядом и стоит дёшево, а три вопроса подряд стоят дороже
|
||||
одного разговора;
|
||||
- развилка найдена **после** последнего чекпоинта — старое правило: **запиши
|
||||
вопрос и доведи остаток**, не останавливаясь.
|
||||
|
||||
Запись вопроса устроена так:
|
||||
|
||||
1. **Запиши там, где проект держит вопросы** (секция беклога, файл задачи,
|
||||
трекер — это знает проект). Проект не сказал, куда, — отдельной секцией
|
||||
`Вопросы` в своём докладе, и это тоже исход. Тело отвечает на три вещи: **что
|
||||
именно решить**, **какие есть варианты и цена каждого**, **что стоит, пока
|
||||
решения нет**. Плюс твоя рекомендация — человек чаще соглашается, чем выбирает
|
||||
заново, и готовое суждение экономит ему весь контекст.
|
||||
2. **Переформулируй задачу на остаток** — то, что делается без этого решения.
|
||||
Назови границу: докуда доводим сейчас.
|
||||
3. Доведи остаток до конца и закоммить. Задача не «висит на вопросе», она сделана
|
||||
в объявленных границах.
|
||||
|
||||
**Что остатком не является — правило живёт не здесь.** Канонический текст с обеими
|
||||
оговорками — в плагине `av-dev-tasks`, скилл `av-dev-tasks:groom`, раздел
|
||||
`## Вопрос, блокер, необратимое`, подраздел «Отличать вопрос от застревания».
|
||||
Правило принадлежит управлению задачами, потому что решает **сделана задача или
|
||||
вышла**, — это исход планирования, а не исполнения. **Ссылайся, не
|
||||
пересказывай:** копия, заведённая здесь, уже однажды разошлась с оригиналом и
|
||||
потеряла из перечня самое необратимое — запись **наружу**.
|
||||
|
||||
Коротко, чтобы знать, когда идти читать: остаток проверяется двумя порогами —
|
||||
**материализация нерешённого** (запись состояния, зависящего от неотвеченного
|
||||
вопроса) и **пол по пользе** (из остатка пропала польза, названная в постановке).
|
||||
Оба порога — стоп: первый поднимает решение до начала записи, второй даёт исход
|
||||
«не доведена».
|
||||
|
||||
Плагин `av-dev-tasks` не подключён — правило не отменяется, а становится
|
||||
осторожнее: прежде чем записать зависящее от нерешённого куда бы то ни было —
|
||||
в хранилище, в журнал, в витрину или наружу, — спрашивай человека.
|
||||
|
||||
Нет полезного остатка — задача заканчивается исходом «не доведена», вопрос
|
||||
записан, ничего не коммитится наполовину.
|
||||
|
||||
### Когда спрашивать вне чекпоинтов
|
||||
|
||||
По другому основанию — не «сложное решение», а **необратимое действие**:
|
||||
|
||||
- деплой, выкладка наружу, смена публичного адреса или токенов;
|
||||
- удаление или перезапись рабочих данных, включая подрезку архивов;
|
||||
- всё, что уходит за пределы машины.
|
||||
|
||||
Здесь ошибка не откатывается коммитом, поэтому спрашиваем даже когда решение
|
||||
кажется очевидным.
|
||||
|
||||
Стиль правок — заточка под проект и конвенции, right-size, без золочения.
|
||||
|
||||
## Границы: чем этот скилл не владеет
|
||||
|
||||
- **Беклогом, целями и приоритетами.** Задача приходит извне. Скилл её не
|
||||
выбирает, не приоритизирует, не заводит и не переоценивает.
|
||||
- **Форматом задач.** Индексы руками не правятся, путь к скрипту учёта не
|
||||
выдумывается: этим владеет `av-dev-tasks:tasks` (шаг 11). Закрытие — работа
|
||||
этого скилла, и это осознанное решение с названной ценой: **приёмщик и
|
||||
исполнитель совпали**. Закрытие поэтому **не окончательно** — человек на
|
||||
груминге (`av-dev-tasks:groom`) возвращает задачу `reopen` с причиной, а доклад
|
||||
по критериям приёмки становится единственным, по чему приёмка вообще возможна.
|
||||
- **Заведением задач из урожая ревью.** Отложенные находки отдаются **списком**;
|
||||
превращать их в задачи — работа `av-dev-tasks:tasks`, у него на этот вход
|
||||
отдельный сценарий «задачи из ревью и аудита». Плагина нет — урожай остаётся
|
||||
списком в докладе, и это говорится строкой.
|
||||
- **Определением ценности.** «Нужна ли эта функциональность» — не вопрос этого
|
||||
скилла ни на одном шаге. Чекпоинт спрашивает «так ли решаем», а не «надо ли».
|
||||
|
||||
## Наблюдаемые исходы
|
||||
|
||||
Ровно четыре, и каждый обязан быть назван в докладе прямо:
|
||||
|
||||
- **сделана** — определение сделанного выполнено целиком;
|
||||
- **не доведена** — с причиной и с записанным вопросом; что именно сделано и до
|
||||
какой границы, названо явно. Сюда же попадает чекпоинт, на котором человек
|
||||
решение не одобрил;
|
||||
- **оказалась крупнее задачи** — распознаётся **до заведения change**, иначе его
|
||||
придётся выбрасывать. Дальше — декомпозиция, и это не работа этого скилла;
|
||||
- **знание вместо изменения** — только у исследовательской ветки: ответ записан
|
||||
по названному адресу, кода задача не потребовала. Это полноправный исход, а не
|
||||
недоведённая работа.
|
||||
|
||||
## Определение сделанного
|
||||
|
||||
Задача сделана, когда верно всё:
|
||||
|
||||
1. гейт проекта зелёный;
|
||||
2. ревью проведено, **план прогона сверен с исходом по каждой теме**, темы без
|
||||
отчёта и без дома названы в границах покрытия;
|
||||
3. решение прошло чекпоинт и не разошлось с одобренным — либо разошлось, и
|
||||
чекпоинт был пройден заново;
|
||||
4. change заархивирован, дельты влиты в актуальные спеки;
|
||||
5. коммит сделан в текущую ветку;
|
||||
6. **критерии приёмки, если проект их дал, выписаны поимённо, и по каждому назван
|
||||
оракул и наблюдаемый исход** — «прогнал вот это, увидел вот то». Это **доклад,
|
||||
а не сертификация: приёмка — не работа этого скилла.** Исполнитель, ставящий
|
||||
себе галочку «принято», проверяет свою работу своим же взглядом — по границе
|
||||
это может делать только приёмщик, разведённый с исполнителем. Критерии приходят
|
||||
снаружи; скилл их не сочиняет и не занижает. Расхождение «по каждому критерию
|
||||
исход есть, а суть задачи не достигнута» — дефект критериев, и о нём
|
||||
сообщается, а не молча дорабатывается.
|
||||
|
||||
У разведки, кончившейся знанием, определение своё и короткое: **ответ записан по
|
||||
адресу, который назвала задача**, и в нём есть провенанс у каждого числа.
|
||||
|
||||
## Исследовательская ветка
|
||||
|
||||
### Р1. Понять вопрос
|
||||
|
||||
Прочитай запись. У типа `research` в ней два обязательных раздела, и оба нужны
|
||||
тебе прямо сейчас: **«Вопрос»** — на что отвечаем, **«Куда ляжет ответ»** — по
|
||||
какому адресу он ляжет. Адрес назван заранее не из аккуратности: ответ, не
|
||||
имеющий дома, остаётся в переписке, и через квартал разведку заказывают заново.
|
||||
|
||||
Вопроса нет — исход «не доведена» с причиной «запись это сырьё»: назови, что
|
||||
нужно дописать, и остановись. Адреса нет, а вопрос есть — назначь адрес сам и
|
||||
скажи об этом строкой: разведка без дома для ответа хуже несделанной.
|
||||
|
||||
Задача не из каталога (пришла текстом) — адреса у неё нет по построению. Тогда
|
||||
дом ответа — `design.md` того change, который родится дальше; кода не будет —
|
||||
`docs/research/`, и это тоже говорится строкой.
|
||||
|
||||
### Р2. Груминг — `opsx:explore`
|
||||
|
||||
Вызови Skill `opsx:explore`. Читай документы проекта, а не только запись:
|
||||
граница домена и «чем проект **не** является» из паспорта отсекают половину
|
||||
вариантов до того, как их начнут сравнивать. **В explore не пишем код.**
|
||||
|
||||
Развилку грумминга **не записывай вопросом** — она и есть предмет следующего
|
||||
шага. Это отличие от обычной ветки: там развилка уходит в запись, здесь она
|
||||
копится в чекпоинт.
|
||||
|
||||
### Р3. Чекпоинт: варианты
|
||||
|
||||
**Остановись и покажи человеку способы решить.** Это первый из двух плановых
|
||||
стопов, и он существует потому, что после `propose` выбор уже сделан
|
||||
предложением.
|
||||
|
||||
Форма — короткая, экран текста:
|
||||
|
||||
- **вопрос**, на который отвечаем, одной фразой (он уже есть в записи);
|
||||
- **2–4 варианта**, не больше. Больше четырёх — это не выбор, а список: человек
|
||||
не сравнит, а признает свою неспособность сравнить и попросит рекомендацию.
|
||||
У каждого варианта: в чём суть простыми словами, **что даёт**, **чего стоит**,
|
||||
**что становится невозможным** (это ловится хуже всего и стоит дороже всего);
|
||||
- **рекомендация с причиной**. Человек чаще соглашается, чем выбирает заново;
|
||||
- что известно **недостоверно** и как это проверить, если проверять дёшево.
|
||||
|
||||
Что нельзя: приносить варианты, различающиеся только реализацией; прятать
|
||||
отвергнутое (отвергнутый вариант с названной причиной — половина будущего ADR);
|
||||
приносить один вариант и называть это выбором.
|
||||
|
||||
**Выбор оседает по адресу.** У `research` это раздел «Куда ляжет ответ». У
|
||||
остальных — `design.md` change, который родится на шаге 2, разделом
|
||||
«рассмотренные варианты». Не в переписку: разговор, из которого ничего не
|
||||
записано, повторяется через месяц целиком.
|
||||
|
||||
Три исхода чекпоинта:
|
||||
|
||||
- **выбран способ** — идёшь на шаг 1 общей ветки;
|
||||
- **ответ и есть исход** — работа кончается знанием: запиши ответ по адресу,
|
||||
доложи исход «знание вместо изменения» и закрой задачу (шаг 11). Провенанс у
|
||||
каждого числа обязателен: число без источника проход ревью обязан читать как
|
||||
условие, а не как замер;
|
||||
- **разведка родила задачи** — исход «оказалась крупнее задачи». Нарезка не твоя
|
||||
работа: отдай список формулировками и остановись.
|
||||
|
||||
## Шаги
|
||||
|
||||
### 1. Прочитать задачу
|
||||
|
||||
Прочитай запись и связанные спеки и черновики.
|
||||
|
||||
Проект даёт задаче **критерии приёмки** — выпиши их сразу: на шаге 2 они уезжают
|
||||
в `tasks.md` change. Файл задачи может быть удалён до коммита, а критерии обязаны
|
||||
его пережить.
|
||||
|
||||
Здесь же проверка на «крупнее задачи»: видно, что одним заходом это не
|
||||
мерджится, — объявляй исход **до** заведения change.
|
||||
|
||||
### 2. Завести change — `opsx:propose`
|
||||
|
||||
Вызови Skill `opsx:propose`. Получаем `proposal.md`, дизайн, дельта-спеки
|
||||
(`ADDED`/`MODIFIED`/`REMOVED Requirements`), `tasks.md`. Каждое `### Requirement`
|
||||
содержит `SHALL`/`MUST`; структурные заголовки английские, сценарии
|
||||
`GIVEN/WHEN/THEN`. Прогони `openspec validate --strict <id>`.
|
||||
|
||||
Критерии приёмки задачи, если они были, копируются в `tasks.md` отдельным блоком.
|
||||
Варианты, разобранные на чекпоинте Р3, — в `design.md`, с причиной отказа по
|
||||
каждому отвергнутому.
|
||||
|
||||
**`proposal.md` пишется так, чтобы его понял человек, не читавший спек.** Это не
|
||||
стилистическое пожелание: из него собирается чекпоинт шага 5, и переписывать его
|
||||
там заново значит завести второй дом для одного объяснения. Требование стоит в
|
||||
`openspec/config.yaml`, `rules.proposal` — то есть применяется в момент
|
||||
порождения артефакта, а не вспоминается после.
|
||||
|
||||
### 3. Разметка задачи — агент `review-scope`
|
||||
|
||||
**Один запуск на всю задачу, и он обслуживает оба чекпоинта ревью.** Запусти
|
||||
агента `review-scope`, дав ему корень проекта, идентификатор change, базу диффа и
|
||||
запись задачи. Кода на этот момент нет, и это условие его работы, а не помеха.
|
||||
|
||||
Он возвращает **план задачи**:
|
||||
|
||||
- **размер** (малое / среднее / крупное) и **сложность** (знакомое /
|
||||
незнакомое), каждое с обоснованием по факту;
|
||||
- **метку** как максимум по двум осям: `small`, `medium` или `large`;
|
||||
- **состав ревью дизайна** — что звать на шаге 4;
|
||||
- **таблицу тем** «тема → дом → глубина → кто закрывает» — для шага 7;
|
||||
- разнесение документов проекта по трём категориям и строку про директивы.
|
||||
|
||||
**Метку выбираешь не ты.** Раньше состав ревью дизайна называл сам оркестратор —
|
||||
то есть тот, кто только что довёл предложение до `propose`. Разведённости с
|
||||
автором в этой точке не было вовсе; теперь есть.
|
||||
|
||||
**План держи в контексте до конца задачи.** На диск он не пишется: файл-план стал
|
||||
бы четвёртым артефактом рядом с `proposal.md`, `tasks.md` и `design.md`, пережил
|
||||
бы задачу и разошёлся бы с ней молча. Прервался прогон — повтори шаг 3, это самый
|
||||
дешёвый его проход.
|
||||
|
||||
**Разметка повторяется ровно в одном случае** — если правки изменили сами
|
||||
**дельта-спеки**: план выведен из них, и план по отменённым требованиям назовёт
|
||||
не те темы. Во всех прочих случаях, включая переделку формы кода на шаге 7,
|
||||
метка остаётся прежней.
|
||||
|
||||
### 4. Ревью дизайна — ДО кода, состав по метке
|
||||
|
||||
Вызови Skill **`av-dev-code:review`**, дав ссылку на change `<id>`,
|
||||
**план разметки с шага 3** и указание, что это ревью дизайна.
|
||||
|
||||
Состав приходит планом, а не решается здесь:
|
||||
|
||||
| Метка | Проходы на предложении |
|
||||
|---|---|
|
||||
| `small` | `specs` |
|
||||
| `medium` | `specs`, `rubric` |
|
||||
| `large` | `specs`, `rubric`, `architecture` + вопрос автору о трёх формах решения |
|
||||
|
||||
`review-specs` в режиме «дизайн ДО кода» идёт **на каждой задаче**: это самый
|
||||
дешёвый проход конвейера, и он ловит то, что на готовом коде уже не чинят.
|
||||
Остальные включаются меткой, потому что стадия стоит на каждой задаче и каждый
|
||||
лишний проход здесь умножается на число задач.
|
||||
|
||||
Смысл стадии: архитектурная находка на готовом коде стоит переписывания и потому
|
||||
игнорируется — та же находка здесь стоит абзаца обсуждения. Если `review-rubric`
|
||||
запускался, перенеси его рубрику в `tasks.md` как приёмочные критерии; там же уже
|
||||
лежат критерии от постановки, если они были.
|
||||
|
||||
**Отработка замечаний, и она идёт до чекпоинта, а не после:**
|
||||
|
||||
- мелочь и явные улучшения — правь сам в спеках и дизайне;
|
||||
- развилки (компромисс, scope, инвариант) — **не в запись, а в чекпоинт**: он
|
||||
следующим шагом, и это ровно то, ради чего он поставлен здесь;
|
||||
- после правок перепрогони `openspec validate --strict <id>`.
|
||||
|
||||
### 5. Чекпоинт: объяснение
|
||||
|
||||
**Остановись и объясни человеку, что происходит.** Второй плановый стоп и
|
||||
единственный обязательный для всех задач.
|
||||
|
||||
Он стоит **после** ревью дизайна намеренно. Человек читает объяснение, уже
|
||||
просеянное машиной: то, что поймал бы `review-specs`, до него не доходит, а
|
||||
внимание — самый дорогой ресурс процесса, и тратить его на выловимое машиной
|
||||
нельзя.
|
||||
|
||||
**Объяснение не сочиняется заново — оно собирается из артефактов**, `proposal.md`
|
||||
и `design.md`. Третий пересказ был бы третьим домом одного и того же и разошёлся
|
||||
бы с обоими. Что показываешь:
|
||||
|
||||
- **в чём проблема** — словами домена из паспорта, без имён модулей и функций;
|
||||
- **как решаем** — суть одним абзацем. Не пересказ дельта-спек: спека нормирует,
|
||||
а здесь объясняют;
|
||||
- **что человек увидит иначе**, когда это будет сделано;
|
||||
- **чего мы намеренно не делаем** и почему — граница scope ловится хуже всего;
|
||||
- **чем рискуем и что осталось нерешённым** — сюда съезжаются развилки,
|
||||
накопленные до этого места, и находки ревью с пометкой `развилка`;
|
||||
- **что дальше**, если возражений нет.
|
||||
|
||||
Проверка на «простой язык» одна и механическая: **в тексте нет `SHALL`, нет имён
|
||||
файлов и функций там, где вещь называется по-русски, и нет слов, которых нет в
|
||||
паспорте проекта.** Не проходит — переписывай, а не объясняй, почему иначе
|
||||
нельзя.
|
||||
|
||||
Длина — экран. Длинное объяснение не читают, а пролистывают, и чекпоинт
|
||||
превращается в ритуал одобрения.
|
||||
|
||||
Три исхода:
|
||||
|
||||
- **согласен** — идёшь на шаг 6;
|
||||
- **скорректировать** — правишь спеки и дизайн по сказанному. Изменились
|
||||
**дельта-спеки** — повтори шаг 3 (разметка выведена из них) и ту часть ревью
|
||||
дизайна, которой касается правка; затем чекпоинт **заново**. Правка внутри
|
||||
дизайна без спек — повтори только чекпоинт;
|
||||
- **не одобрено** — исход «не доведена» с причиной. Change остаётся
|
||||
незаархивированным, задача не закрывается, ничего не коммитится наполовину.
|
||||
|
||||
### 6. Написать код — `opsx:apply`
|
||||
|
||||
Вызови Skill `opsx:apply` для реализации `tasks.md`. Код — по конвенциям проекта
|
||||
(каталог `docs/conventions/`). Меняешь схему — обнови её описание в документации
|
||||
тем же change, если проект этого требует: гейт обычно это проверяет.
|
||||
|
||||
Прогони гейт и добейся зелёного — он же гейт следующего шага.
|
||||
|
||||
**Поведенческая верификация.** Если задача меняет реальное поведение (новый
|
||||
эндпоинт, разбор входа, схема, форма ответа) — зелёных юнит-тестов мало. Подними
|
||||
изменение вживую командой из раздела команд `CLAUDE.md` и прогони сценарий.
|
||||
Пропусти только для чисто внутренних правок без наблюдаемого рантайма.
|
||||
|
||||
**Сервис не оставляем лежать.** Если запуск упал — почини или откати до конца
|
||||
шага.
|
||||
|
||||
### 7. Ревью кода — та же метка
|
||||
|
||||
Вызови Skill **`av-dev-code:review`**, дав ссылку на change `<id>`,
|
||||
базу диффа, **план разметки с шага 3** и режим запуска.
|
||||
|
||||
**Метку ты не выбираешь, и это правило, а не упрощение.** Её назвал
|
||||
`review-scope` ещё на шаге 3 — по размеру и сложности, с обоснованием по каждой
|
||||
оси. Причина в разведённости: ты только что написал этот код, и решать, насколько
|
||||
глубоко его проверять, тебе нельзя — под давлением «я почти закончил» решение
|
||||
известно заранее. Правило выбора живёт в скилле конвейера —
|
||||
`av-dev-code:review`, `references/review-levels.md`; проектные
|
||||
триггеры — в `docs/review.*`, подраздел «Триггеры метки».
|
||||
|
||||
**Метка не пересматривается по факту диффа.** Дифф может выйти крупнее, чем
|
||||
ожидалось при разметке, — это не повод её поднимать: пересмотр означал бы второй
|
||||
запуск разметчика, ровно то, ради устранения чего он и переехал на шаг 3.
|
||||
|
||||
**Считаешь метку заниженной — скажи это в докладе строкой, а не переспорь.**
|
||||
Разметчик вправе и поднять, и понизить; твоё несогласие это факт для человека, а
|
||||
не команда конвейеру. Место, где такое несогласие превращается в изменение
|
||||
правил, — журнал дефектов `docs/review.md`, и только постфактум.
|
||||
|
||||
**Плана нет — ревью кода не запускается.** Триаж требует план обязательным
|
||||
входом: без него он не может сверить, все ли размеченные темы вернули отчёт, а
|
||||
эта сверка — единственная защита от молчащего пропуска. Потерял план (прервалась
|
||||
сессия, ушёл контекст) — повтори шаг 3, а не гони прогон без него.
|
||||
|
||||
**Режим по умолчанию — `по графу`, и обосновывать его не надо.** Конвейер сам
|
||||
знает свои рёбра: гейт открывает опиниативные проходы, проходы с пометкой «держит
|
||||
машину» идут цепочкой (иначе замеры портят друг друга и находка выглядит
|
||||
доказанной), триаж — сток. Просить **`линейно`** нужно только по причине, и она
|
||||
называется строкой: так сказал оператор; машина занята чем-то ещё; идёт разбор
|
||||
самого конвейера.
|
||||
|
||||
Скилл сам гоняет гейт, нужные проходы и обязательный триаж. Возвращает отчёт с
|
||||
потолком 7 пунктов, разметкой `Действие: инлайн | развилка` и секцией границ
|
||||
покрытия.
|
||||
|
||||
**Сверь план прогона с исходом, прежде чем коммитить.** Отчёт начинается планом
|
||||
разметчика — таблицей «тема → дом → глубина → кто закрывает», — и против каждой
|
||||
темы обязан стоять исход. Тема без отчёта и тема без дома — разные вещи, и обе
|
||||
должны быть названы. Реестр короткий (шесть тем ядра плюс свои) — сверка стоит
|
||||
одного взгляда.
|
||||
|
||||
#### Отработка, и здесь появляется одно новое правило
|
||||
|
||||
Помеченное `инлайн` чини сам и не логируй. `развилка` — вопросом в запись (он уже
|
||||
сформулирован триажем, его остаётся перенести). После правок — снова гейт.
|
||||
|
||||
**Находка, отменяющая одобренный дизайн, отменяет и одобрение.** Признак
|
||||
проверяемый: **меняются ли дельта-спеки**.
|
||||
|
||||
- не меняются — находка внутри дизайна, дожимай сам, это обычная отработка;
|
||||
- меняются — решение стало другим, а одобрено было прежнее. Повтори шаг 3
|
||||
(разметка выведена из дельта-спек) и **вернись на чекпоинт шага 5** с тем, что
|
||||
изменилось и почему.
|
||||
|
||||
**Это правило старше правила о развилке.** Находка класса `развилка`, чьё
|
||||
основание — «надо менять спеку», подпадает под оба; побеждает возврат на
|
||||
чекпоинт, а вопрос в запись при нём остаётся, но возврата не заменяет. Иначе
|
||||
одобренный дизайн переделывался бы записанным вопросом, то есть молча.
|
||||
|
||||
Тихо переделать утверждённое нельзя. Чекпоинт, который можно обойти находкой
|
||||
ревью, не значит ничего, а человек при этом уверен, что одобрил именно то, что
|
||||
уехало в коммит.
|
||||
|
||||
**Урожай — списком, не задачами.** Отложенные находки (реальный `major` не для
|
||||
этого мерджа, развилка, решённая «потом», пачка `nit`) собери в секцию доклада
|
||||
`Урожай`: формулировка, оракул, провенанс. Задачи из него **заводит не этот
|
||||
скилл** — их заводит `av-dev-tasks:tasks` своим сценарием «задачи из ревью и
|
||||
аудита»: своя нарезка, свой формат, свои правила дублей. Твоя обязанность — не
|
||||
потерять и передать.
|
||||
|
||||
**Границы покрытия из отчёта не выбрасывай** — они уезжают в финальный доклад
|
||||
сжатой строкой. Отчёт, из которого исчезло «что проверить было невозможно»,
|
||||
превращается в ложное ощущение проверенности.
|
||||
|
||||
**Отчёт триажа сохрани вместе с change (`openspec/changes/<id>/review/`; шаг 8
|
||||
унесёт его в архив вместе с change) — это обязательно, а не «если удобно».** По
|
||||
нему потом видно, что было найдено и что из этого осталось в урожае. И это
|
||||
единственный **независимый** артефакт о составе прогона: своей прозе здесь верить
|
||||
нельзя — она написана тем же, кто мог проход и пропустить.
|
||||
|
||||
### 8. Архивировать — `opsx:archive`
|
||||
|
||||
Вызови Skill `opsx:archive`: change уезжает в архив, дельты вливаются в
|
||||
актуальные спеки. Не пропускай `openspec validate --strict` перед этим.
|
||||
|
||||
### 9. Синк документации
|
||||
|
||||
**Вызови Skill `av-dev-docs:docs`**: он владеет содержимым документов канона и
|
||||
ведёт чек-лист синка.
|
||||
|
||||
**Правило одно и оно жёсткое: принуждённое отрицание.** Доклад обязан назвать
|
||||
**каждый** документ канона — либо чем он обновлён, либо «не требуется, потому
|
||||
что…». Нетронутые группируются одной строкой с общей причиной. Список триггеров
|
||||
прозой уже проверен на живом проекте и дал 6 записей ADR на 43 изменения;
|
||||
работает только обязательное отрицание.
|
||||
|
||||
**Список документов и их триггеров здесь не дублируется** — он в чек-листе скилла
|
||||
`av-dev-docs:docs`, и копия уже однажды разошлась с оригиналом, потеряв два
|
||||
триггера.
|
||||
|
||||
**Плагина `av-dev-docs` в проекте нет** — путь в его дерево не разрешится ниоткуда,
|
||||
поэтому за списком иди в **свой** reference:
|
||||
[references/project-facts.md](../review/references/project-facts.md)
|
||||
конвейера ревью перечисляет все документы канона с их предметом. Пройди по этому
|
||||
перечню — каждый документ получает строку, отрицание остаётся обязательным.
|
||||
|
||||
**Двух домов в том перечне нет намеренно, а синку они нужны: `docs/adr/` и
|
||||
`docs/research/`.** Проход ревью их не открывает — процессные, — но ADR как раз
|
||||
**главный выход синка**: решение, принятое по ходу задачи, без этой строки
|
||||
теряется систематически. Добавь их к перечню руками: `adr/` — решение с ценой,
|
||||
принятое в этой задаче; `research/` — записка разведки, если ветка была
|
||||
исследовательской.
|
||||
Триггеры при этом ты знаешь хуже, и это называется в докладе строкой: «синк
|
||||
сделан по перечню документов, без списка триггеров — плагина `av-dev-docs` нет».
|
||||
Канона в проекте тоже нет — назови это исходом и предложи `av-dev-docs:canon`.
|
||||
|
||||
### 10. Коммит
|
||||
|
||||
Коммить **в текущую ветку** (`git rev-parse --abbrev-ref HEAD`), сам ветку не
|
||||
создавай и не переключай, ничего не пушь.
|
||||
|
||||
Сообщение — по-русски, **скиллом `av-dev-git:commit`**: форму сообщения держит
|
||||
он, и здесь она не пересказывается. Вызов не разрешился — плагина в проекте нет:
|
||||
напиши сообщение сам и скажи строкой доклада, что форму коммита не сверял никто.
|
||||
Одна задача — один осмысленный коммит.
|
||||
|
||||
### 11. Закрыть задачу — **после коммита, не раньше**
|
||||
|
||||
**Вызови Skill `av-dev-tasks:tasks`** и попроси закрыть задачу как реализованную —
|
||||
он владеет форматом и двигает строку индекса сам. Путь к его скрипту не выясняй и
|
||||
индексы руками не правь: мост между плагинами — вызов скилла, а не путь.
|
||||
|
||||
**Порядок обязателен.** Закрытие удаляет файл задачи; сделанное до коммита оно
|
||||
оставило бы задачу закрытой без единого следа работы, если шаг 10 упадёт.
|
||||
|
||||
**Закрытие тоже коммитится — вторым коммитом, тут же.** Удаление файла задачи и
|
||||
правка индексов (их имена знает `av-dev-tasks`, не ты) — это правки в рабочем
|
||||
дереве, и оставить их незакоммиченными нельзя: закрытие, не доехавшее до основной
|
||||
ветки, оставит задачу открытой молча, а опора «индексы под git показывают, что и
|
||||
когда закрыто» без коммита — пустые слова. Сообщение короткое, про учёт, а не про
|
||||
работу: `закрыта задача <slug>`. Это второй коммит осознанно: правило «одна задача
|
||||
— один осмысленный коммит» про работу, а учёт — не работа.
|
||||
|
||||
Разведка, кончившаяся знанием, закрывается так же — но перед этим убедись, что
|
||||
ответ **записан по названному адресу и закоммичен**. Закрытая разведка без
|
||||
записанного ответа не оставляет следа вообще: файл задачи удалён, ответ был в
|
||||
переписке.
|
||||
|
||||
Плагина в проекте нет — вызов не разрешится. Тогда **ничего не выдумывай**: скажи
|
||||
в докладе, что учёт задач остаётся за владельцем, и назови исход.
|
||||
|
||||
## Доклад
|
||||
|
||||
Коротко, и в нём обязательно:
|
||||
|
||||
- **исход** одним из четырёх слов и, если не «сделана», чем ограничен результат;
|
||||
- что сделано, какие вопросы записаны и куда;
|
||||
- **что было одобрено на чекпоинте и совпало ли с тем, что уехало в коммит** —
|
||||
расхождение здесь называется прямо, даже если оно мелкое;
|
||||
- ссылка на архивный change и хеш коммита;
|
||||
- по каждому критерию приёмки, если они были: **оракул и наблюдаемый исход** —
|
||||
это доклад приёмщику, а не отметка «принято»;
|
||||
- **`Урожай`** — отложенные находки списком (формулировка, оракул, провенанс);
|
||||
- **одна строка границ покрытия**: какая метка и режим гонялись, какие проходы не
|
||||
запускались и что проверить было невозможно. Доклад без неё сообщает
|
||||
«проверено», не сообщая, что именно.
|
||||
|
||||
## Тонкости
|
||||
|
||||
- **Не завязывайся на основную ветку и корень репозитория.** Скилл работает в
|
||||
текущем worktree и на текущей ветке: не делай `git checkout`/`switch`, не
|
||||
создавай веток, не пушь.
|
||||
- Обычная задача с очевидным решением проходит **один** чекпоинт, и это норма, а
|
||||
не упрощение. Два чекпоинта — цена незнания, а не признак серьёзности задачи.
|
||||
- Гейт блокирует: пока он красный, опиниативные проходы не запускаются. Чинить и
|
||||
перезапускать, а не «посмотреть заодно».
|
||||
- Держи вызывающего в цикле короткими репликами на переходах фаз, но не проси
|
||||
подтверждать механику: чекпоинты — единственные места, где ждут ответа.
|
||||
- **Занизить метку ревью, пропустить тему или проскочить чекпоинт — самый дешёвый
|
||||
способ «ускориться», и он же самый дорогой по последствиям.** Защита устроена
|
||||
так, что регулятора у тебя нет: метку выбирает разметчик **до того**, как ты
|
||||
написал код, план сверяется по темам, непокрытое называется строкой, а
|
||||
расхождение с одобренным — отдельным пунктом доклада.
|
||||
+77
-40
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: review-pipeline
|
||||
description: "Конвейер ревью изменения, устроенный по темам: документ проекта либо заводит тему ревью, либо питает чужую тему источником, либо процессный и в ревью не читается вовсе. Ядро тем — requirements, autotests, conventions, architecture, security, operations; список тем открытый, свои темы проект заводит документом. Разметка задачи идёт один раз, после propose: агент review-scope выводит размер и сложность, из их максимума — метка, и раздаёт темы проходам обеих стадий. Метка правит и ревью дизайна (small — только specs; medium — плюс rubric; large — плюс architecture), и ревью кода (small — гейт, спеки, код, триаж; medium — плюс приёмник тем; large — плюс доказательство: враждебные постановки, эксплуатационный постмортем, архитектурный проход на широком входе). Триаж обязателен всегда. Порядок прогона — граф зависимостей: гейт открывает опиниативные проходы, проходы с пометкой «держит машину» идут цепочкой, триаж — единственный сток. Проектная специфика приходит из документов канона av-dev-pm. Вызывается из task-pipeline (чекпоинты ревью) и из task-batch (финальная сверка)."
|
||||
name: review
|
||||
description: "Конвейер ревью изменения, устроенный по темам: документ проекта либо заводит тему ревью, либо питает чужую тему источником, либо процессный и в ревью не читается вовсе. Ядро тем — requirements, autotests, conventions, architecture, security, operations; список тем открытый, свои темы проект заводит документом. Разметка задачи идёт один раз, после propose: агент review-scope выводит размер и сложность, из их максимума — метка, и раздаёт темы проходам обеих стадий. Метка правит и ревью дизайна (small — только specs; medium — плюс rubric; large — плюс architecture), и ревью кода (small — гейт, спеки, код, триаж; medium — плюс приёмник тем; large — плюс доказательство: враждебные постановки, эксплуатационный постмортем, архитектурный проход на широком входе). Триаж обязателен всегда. Порядок прогона — граф зависимостей: гейт открывает опиниативные проходы, проходы с пометкой «держит машину» идут цепочкой, триаж — единственный сток. Проектная специфика приходит из документов канона av-dev-docs. Вызывается из скилла resolve — двумя стадиями: ревью дизайна до кода и ревью кода после apply."
|
||||
---
|
||||
|
||||
# Конвейер ревью
|
||||
@@ -44,7 +44,7 @@ description: "Конвейер ревью изменения, устроенны
|
||||
|
||||
- **OpenSpec — жёсткая предпосылка, а не опция.** Ревью дизайна, проход
|
||||
`review-specs` и
|
||||
вызывающий пайплайн задачи завязаны на дельта-спеки
|
||||
вызывающий скилл `av-dev-code:resolve` завязаны на дельта-спеки
|
||||
(`openspec/changes/<id>/specs/*/spec.md`), на актуальные спеки
|
||||
(`openspec/specs/`) и на `openspec validate --strict`. В проекте без OpenSpec
|
||||
шаги, зовущие `opsx:explore` / `opsx:propose` / `opsx:apply` / `opsx:archive`,
|
||||
@@ -52,17 +52,50 @@ description: "Конвейер ревью изменения, устроенны
|
||||
требований. **Проект без OpenSpec этим конвейером не проверяется** — подключай
|
||||
OpenSpec, а не понижай прогон: ветка деградации здесь не пишется, потому что
|
||||
непроверенная ветка деградации хуже честного отказа. Заводить руками не надо:
|
||||
`av-dev-pm:init` делает `openspec init` на новом проекте, `canon adopt` — на
|
||||
переводимом, и оба кладут `openspec/config.yaml` канонической формы.
|
||||
этим владеет скилл `av-dev-code:openspec` — он заводит каталог и заменяет
|
||||
пример в `config.yaml` настройкой. Его же зовут `av-dev-docs:init` на новом
|
||||
проекте и `av-dev-docs:canon` в режиме `adopt` — на переводимом.
|
||||
- **Документы канона** — см. следующий раздел.
|
||||
- **Проектные копии этих скиллов и агентов удаляются при установке.** Если в
|
||||
проекте уже лежат свои `.claude/skills/review-pipeline`,
|
||||
`.claude/skills/task-pipeline`, `.claude/skills/task-batch` или
|
||||
проекте уже лежат свои `.claude/skills/review`,
|
||||
`.claude/skills/review-pipeline`, `.claude/skills/task-pipeline`,
|
||||
`.claude/skills/task-batch`, `.claude/skills/resolve` или
|
||||
`.claude/agents/<проект>-review-*.md` — снеси их. Иначе короткое имя разрешится
|
||||
в устаревшую проектную копию, молча и без признаков подмены. По той же причине
|
||||
**скиллы этого плагина зовутся с пространством имён**:
|
||||
`av-dev-pipeline:review-pipeline`, `av-dev-pipeline:task-pipeline`,
|
||||
`av-dev-pipeline:task-batch`.
|
||||
в устаревшую проектную копию, молча и без признаков подмены.
|
||||
|
||||
### Обращение к соседним плагинам
|
||||
|
||||
**Копия.** Дом — `shared/plugin-boundary.md` в репозитории плагинов. Правится
|
||||
дом, а не этот файл.
|
||||
|
||||
<!-- копия: граница-плагинов из shared/plugin-boundary.md -->
|
||||
|
||||
Плагины `av-dev` ставятся порознь, и ни один не вправе считать, что сосед на
|
||||
месте.
|
||||
|
||||
**Чужой скилл зовётся полным именем** — `av-dev-docs:canon`, `av-dev-tasks:tasks`,
|
||||
`av-dev-code:review`. Короткое имя может разрешиться в устаревшую проектную копию
|
||||
из `.claude/skills/`, и подмены не будет видно ни в докладе, ни в поведении.
|
||||
|
||||
**Путь в дерево чужого плагина не пишется никогда.** `$CLAUDE_PLUGIN_ROOT` ведёт
|
||||
только в свой плагин; вычисленный от него путь к соседу либо не откроется, либо
|
||||
откроет чужую установку. Нужен чужой справочник — зови владеющий им скилл, он
|
||||
прочитает его сам.
|
||||
|
||||
**Вызов не разрешился — плагина в проекте нет.** Это исход, а не поломка: назови
|
||||
строкой доклада, чего теперь не делает никто, и продолжай работу. Молчать нельзя,
|
||||
пропуск неотличим от сделанного; выдумывать обходной путь нельзя тоже.
|
||||
|
||||
**Присутствие узнаётся вызовом или следом в проекте, но не объявлением.** Перечня
|
||||
установленных плагинов проект не ведёт — он разошёлся бы с действительностью
|
||||
молча. Что сосед здесь работал, видно по заведённому им файлу: `docs/.pm.json` —
|
||||
канон, `<каталог задач>/.tasks.json` — задачи, `openspec/config.yaml` — конвейер.
|
||||
|
||||
<!-- /копия: граница-плагинов -->
|
||||
|
||||
Своих скиллов это касается ровно так же: `av-dev-code:review`,
|
||||
`av-dev-code:resolve`, `av-dev-code:openspec` — подменяется короткое имя,
|
||||
а не чужое.
|
||||
|
||||
## Темы, источники и процессные документы
|
||||
|
||||
@@ -80,7 +113,7 @@ description: "Конвейер ревью изменения, устроенны
|
||||
|---|---|---|
|
||||
| **тема** | заводит направление проверки и требует исполнителя | `conventions.*`, `security.*`, `architecture.*`, свои документы проекта |
|
||||
| **источник темы** | читает как материал чужой темы, своей не порождает | `passport.*`, `database.*`, `CLAUDE.md`/`AGENTS.md`, `openspec/specs/` |
|
||||
| **процессный документ** | не судит по нему изменение | `docs/tasks/`, `docs/review.*`, `docs/adr.*`, `docs/research.*`, `docs/.pm.json` |
|
||||
| **процессный документ** | не судит по нему изменение | `tasks/`, `docs/review.*`, `docs/adr.*`, `docs/research.*`, `docs/.pm.json` |
|
||||
|
||||
**Одна процессная запись всё же читается — `docs/review.*`.** В ней лежит
|
||||
настройка самого конвейера: вопросы по темам, журнал дефектов, типовые узлы,
|
||||
@@ -88,8 +121,8 @@ description: "Конвейер ревью изменения, устроенны
|
||||
критерий, по которому судит изменение. `adr.*`, `research.*` и `tasks/` не
|
||||
открывает никто.
|
||||
|
||||
Дом канона этой раскладки — `av-dev-pm`, `references/canon.md`, раздел «Три
|
||||
категории документов». Конвейер её **читатель**: категории и имена тем он берёт
|
||||
Дом канона этой раскладки — скилл `av-dev-docs:canon`, раздел «Три категории
|
||||
документов». Конвейер её **читатель**: категории и имена тем он берёт
|
||||
оттуда и своих не заводит.
|
||||
|
||||
Отсюда то, ради чего правило и заведено: **`docs/` перестаёт быть просто
|
||||
@@ -120,7 +153,7 @@ description: "Конвейер ревью изменения, устроенны
|
||||
читал решения, а эксплуатационный и `specs` — числа. Цена решения записана в
|
||||
каноне и повторяется здесь, потому что платит её конвейер: **расхождение
|
||||
изменения с записанным решением прогоном не ловится**, это работа сверки
|
||||
документации (`av-dev-pm`, агент `doc-consistency`) на сессии между спринтами.
|
||||
документации — скилл `av-dev-docs:healthcheck`.
|
||||
Строка об этом обязательна в границах покрытия каждого прогона.
|
||||
|
||||
**Своя тема проекта бывает двух происхождений, и обе законны:** документ, который
|
||||
@@ -147,7 +180,7 @@ description: "Конвейер ревью изменения, устроенны
|
||||
дом для тех же фактов разошёлся бы и выглядел актуальным.
|
||||
|
||||
**Документов канона нет вовсе** — проект не приведён к канону. Скажи это строкой
|
||||
и предложи скилл `av-dev-pm:canon`: одна операция на проект против деградации на
|
||||
и предложи скилл `av-dev-docs:canon`: одна операция на проект против деградации на
|
||||
каждой задаче. Прогон при этом не останавливается.
|
||||
|
||||
## Что получает каждый проход
|
||||
@@ -163,7 +196,7 @@ description: "Конвейер ревью изменения, устроенны
|
||||
прохода между метками;
|
||||
- **контракт находок** — путь к
|
||||
[references/finding-contract.md](references/finding-contract.md) (в
|
||||
установленном плагине — `${CLAUDE_PLUGIN_ROOT}/skills/review-pipeline/references/`);
|
||||
установленном плагине — `${CLAUDE_PLUGIN_ROOT}/skills/review/references/`);
|
||||
- **изменение** — идентификатор change и путь к его дельта-спекам;
|
||||
- **база диффа**;
|
||||
- **метка, его глубина и режим** прогона — чтобы проход знал, что писать в
|
||||
@@ -527,8 +560,7 @@ flowchart TD
|
||||
ничего не портит, он только дольше, и домысливать тут нечего;
|
||||
2. **машина занята, и знает об этом вызывающий.** Рядом идёт другая задача,
|
||||
поднят сервис, гоняется дорогая проверка проекта. Сам конвейер занятости
|
||||
машины не видит — её обязан назвать тот, кто запускает; так и делает
|
||||
`av-dev-pipeline:task-batch`, когда ведёт задачи параллельно;
|
||||
машины не видит — её обязан назвать тот, кто запускает;
|
||||
3. **разбор самого конвейера** — когда выясняется, почему проход чего-то не
|
||||
нашёл, порядок и изоляция важнее скорости.
|
||||
|
||||
@@ -592,7 +624,7 @@ flowchart TD
|
||||
|
||||
**План живёт в контексте прогона задачи и на диск не пишется.** Файл-план был бы
|
||||
четвёртым артефактом рядом с `proposal.md`, `tasks.md` и `design.md`, жил бы
|
||||
дольше задачи и расходился бы с ней молча. Прервали пайплайн — разметка
|
||||
дольше задачи и расходился бы с ней молча. Прервали прогон задачи — разметка
|
||||
повторяется; это самый дешёвый проход конвейера, и платить за его вечность
|
||||
дороже, чем перезапустить.
|
||||
|
||||
@@ -816,8 +848,8 @@ Recall темы `conventions` равен длине конвенций прое
|
||||
|
||||
## Ревью дизайна — до кода
|
||||
|
||||
Запускается на первом чекпоинте ревью (шаг 5 скилла
|
||||
`av-dev-pipeline:task-pipeline`), когда change уже имеет `proposal.md` и
|
||||
Запускается на первой стадии ревью (шаг 4 скилла
|
||||
`av-dev-code:resolve`), когда change уже имеет `proposal.md` и
|
||||
дельта-спеки, но кода ещё нет. Разметка задачи к этому моменту уже прошла — она
|
||||
шагом раньше, и метка известна.
|
||||
|
||||
@@ -832,10 +864,11 @@ Recall темы `conventions` равен длине конвенций прое
|
||||
| `large` — крупное или незнакомое | `specs`, `rubric`, `architecture` + вопрос автору | **3** |
|
||||
|
||||
- **всегда** — `review-specs` в режиме «дизайн ДО кода». Дельта-спеки сверяются
|
||||
на каждой задаче: это самый дешёвый чекпоинт конвейера, и он ловит то, что на
|
||||
на каждой задаче: это самый дешёвый проход конвейера, и он ловит то, что на
|
||||
готовом коде уже не чинят;
|
||||
- **со `medium`** — `review-rubric` (фаза 1 без фазы 2: рубрика на задуманный
|
||||
узел становится приёмочными критериями и уезжает в `tasks.md`);
|
||||
- **со `medium`** — `review-rubric`: рубрика на задуманный узел, по ней же
|
||||
разбирается дельта-спека, а сами пункты уезжают приёмочными критериями в
|
||||
`tasks.md`;
|
||||
- **только в `large`** — `review-architecture` на предложении: можно ли выразить
|
||||
существующими понятиями — **включая конструкции стандартной библиотеки**, — не
|
||||
появляется ли второй способ. Вопрос «не изобретаем ли то, что уже есть в
|
||||
@@ -853,14 +886,15 @@ Recall темы `conventions` равен длине конвенций прое
|
||||
отвечается «нет» ещё до запуска. Держать её ниже `large` значит платить за
|
||||
предсказуемый ответ на каждой задаче.
|
||||
|
||||
Причина меток — арифметика, а не экономия на осторожности. Чекпоинт стоит
|
||||
Причина меток — арифметика, а не экономия на осторожности. Стадия стоит
|
||||
**на каждой задаче**, поэтому каждый проход здесь умножается на число задач, и при
|
||||
мелкой нарезке это самая большая статья конвейера.
|
||||
|
||||
**Граф этой стадии свой, и он плоский.** Гейта нет — кода ещё нет, запускать
|
||||
нечего; метка уже названа разметкой задачи; машину не держит ни один проход;
|
||||
сток — не триаж, а шаг пайплайна задачи, где замечания отрабатываются правкой
|
||||
спек. Триаж здесь не нужен: находок единицы, и каждая либо правит спеку, либо
|
||||
сток — не триаж, а шаг скилла `av-dev-code:resolve`, где замечания
|
||||
отрабатываются правкой спек. Триаж здесь не нужен: находок единицы, и каждая
|
||||
либо правит спеку, либо
|
||||
становится развилкой.
|
||||
|
||||
```mermaid
|
||||
@@ -868,10 +902,10 @@ flowchart TD
|
||||
plan[/"план разметки задачи:<br/>размер, сложность, метка"/]
|
||||
proposal["предложение: proposal.md + дельта-спеки"]
|
||||
specs["specs (режим «дизайн ДО кода») — всегда"]
|
||||
rubric["rubric, фаза 1 → приёмочные критерии в tasks.md"]
|
||||
rubric["rubric → приёмочные критерии в tasks.md"]
|
||||
arch["architecture на предложении"]
|
||||
author["вопрос автору: три формы решения и компромисс каждой"]
|
||||
fix["шаг пайплайна: правка спек, развилки — вопросом в запись"]
|
||||
fix["шаг resolve: правка спек, развилки — вопросом в запись"]
|
||||
|
||||
plan --> proposal
|
||||
proposal --> specs
|
||||
@@ -905,13 +939,16 @@ flowchart TD
|
||||
|
||||
- Оркестратор чинит помеченное `Действие: инлайн` и **не логирует мелочь**.
|
||||
- `Действие: развилка` — вопросом с вариантами и ценой каждого туда, где проект
|
||||
держит вопросы (это знает вызвавший пайплайн, а не конвейер). Оркестратор не
|
||||
держит вопросы (это знает вызвавший скилл, а не конвейер ревью). Оркестратор не
|
||||
останавливается: он урезает изменение до остатка и доводит его.
|
||||
- Находка не для этого мерджа, но реальная (отложенный `major`, развилка,
|
||||
решённая «потом»), — не теряется, но **и не заводится здесь**. Конвейер отдаёт
|
||||
её **списком урожая** в отчёте: формулировка, оракул, провенанс (какой проход,
|
||||
какой change). Заведение задач принадлежит тому, кто ведёт задачи проекта, —
|
||||
у него свой формат, своя нарезка и свои правила дублей. Мелочь класса `nit`
|
||||
какой change). Заведение задач принадлежит `av-dev-tasks:tasks` — зови его со
|
||||
списком урожая, у него на этот вход отдельный сценарий «задачи из ревью и
|
||||
аудита»: свой формат, кластеризация по причине, дедуп против беклога и
|
||||
кладбища. Плагина нет — урожай остаётся списком в отчёте, и это говорится
|
||||
строкой доклада: задачи из него не заведёт никто. Мелочь класса `nit`
|
||||
идёт в урожай одной пачкой, а не записью на находку.
|
||||
- `Promote candidates` — по процедуре [references/promote.md](references/promote.md):
|
||||
находка → конвенция → правило линтера → **удаление формулировки из конвенций**.
|
||||
@@ -923,11 +960,11 @@ flowchart TD
|
||||
Он единственное, по чему потом видно, что было найдено и что из этого не
|
||||
заведено: нулевой урожай при непустом отчёте виден сразу.
|
||||
**Вместе с изменением он и переезжает:** после `opsx:archive` его адрес —
|
||||
`openspec/changes/archive/<id>/review/`. Кто ищет отчёт после архивации (батч
|
||||
на финальной сверке, приёмщик на сессии), смотрит **оба** пути; «отчёта нет»
|
||||
объявляется, только когда пуст и архивный, иначе самый дорогой сценарий
|
||||
«состав ревью неизвестен, гоняем заново» срабатывает на каждой доведённой
|
||||
задаче.
|
||||
`openspec/changes/archive/<id>/review/`. Кто ищет отчёт после архивации
|
||||
(приёмщик на груминге `av-dev-tasks:groom`, разбор дефекта), смотрит **оба**
|
||||
пути; «отчёта нет» объявляется, только когда пуст и архивный, иначе самый
|
||||
дорогой сценарий «состав ревью неизвестен, гоняем заново» срабатывает на
|
||||
каждой доведённой задаче.
|
||||
|
||||
## Честный предел
|
||||
|
||||
@@ -994,7 +1031,7 @@ flowchart TD
|
||||
сверкой и доказательством лежит весь класс дефектов, который виден только
|
||||
построенным путём, — и он проверяется на 5–10% задач.
|
||||
|
||||
Это сознательная сделка, а не пробел в устройстве: цес меткой `large` платится на
|
||||
Это сознательная сделка, а не пробел в устройстве: цена метки `large` платится на
|
||||
каждой задаче, а окупается на немногих. Проверяется сделка не рассуждением, а
|
||||
журналом дефектов: если класс, который ловят только меряющие проходы, начал
|
||||
всплывать после мерджа — метку выбирают слишком низко.
|
||||
@@ -1021,7 +1058,7 @@ flowchart TD
|
||||
и где это лежит в документах проекта; таблица поразрядной деградации.
|
||||
- [references/review-levels.md](references/review-levels.md) — дом правила выбора
|
||||
метки: две оси, спорное вниз, чем `small` дешевле, доли как проверка правила.
|
||||
- Skill `av-dev-pm:canon` — приведение проекта к канону документов.
|
||||
- Skill `av-dev-docs:canon` — приведение проекта к канону документов.
|
||||
- [references/finding-contract.md](references/finding-contract.md) — контракт находок.
|
||||
- [references/promote.md](references/promote.md) — промоут находка → конвенция → правило → удаление.
|
||||
- [references/calibration.md](references/calibration.md) — калибровка инъекцией, вердикты keep/retune/drop.
|
||||
+7
-7
@@ -5,12 +5,11 @@
|
||||
выдаёт правдоподобные общие места: их дорого опровергать и нечем подтверждать.
|
||||
|
||||
Отдельного файла-брифа **нет**. Проектная конкретика живёт в документах канона
|
||||
`av-dev-pm`, и проход читает их напрямую: пути жёсткие, посредник не нужен, а
|
||||
`av-dev-docs`, и проход читает их напрямую: пути жёсткие, посредник не нужен, а
|
||||
второй дом для тех же фактов разошёлся бы и выглядел актуальным.
|
||||
|
||||
Определение канона — в плагине `av-dev-pm`,
|
||||
`skills/canon/references/canon.md`. Здесь только карта «тема → её дом → что
|
||||
оттуда берётся».
|
||||
Определение канона держит скилл `av-dev-docs:canon`. Здесь только карта «тема →
|
||||
её дом → что оттуда берётся».
|
||||
|
||||
## Карта тем
|
||||
|
||||
@@ -119,7 +118,7 @@
|
||||
строка неотличима от «мы просто не стали» и перестаёт читаться на третьей задаче.
|
||||
|
||||
**Документов канона нет вовсе** — проект не приведён к канону. Это не повод
|
||||
работать вслепую: скажи об этом строкой и предложи `av-dev-pm:canon`. Одна
|
||||
работать вслепую: скажи об этом строкой и предложи `av-dev-docs:canon`. Одна
|
||||
операция на проект против деградации на каждой задаче.
|
||||
|
||||
## Правило чтения
|
||||
@@ -133,5 +132,6 @@
|
||||
на диск и на СУБД» экономит обязательный вопрос. Отсутствие строки — не факт,
|
||||
а пробел, и его надо назвать в границах покрытия.
|
||||
- **Свойство, ставшее правилом линтера, из конвенций удалено** и лежит в
|
||||
перечне механизированного в `docs/conventions/README.md`. Проверять его
|
||||
проходом — тратить внимание на уже проверенное.
|
||||
перечне механизированного — в `docs/conventions/README.md`, если конвенции
|
||||
каталогом, и отдельным разделом `docs/conventions.md`, если файлом. Проверять
|
||||
его проходом — тратить внимание на уже проверенное.
|
||||
+4
-3
@@ -18,7 +18,7 @@ flowchart TD
|
||||
no["промоуту не подлежит:<br/>место одно — комментарий в коде;<br/>вкусовщина — вон на триаже;<br/>нужен рантайм — в журнал ревью"]
|
||||
conv["конвенция:<br/>проверяемое свойство + какой проход нашёл"]
|
||||
rule["правило линтера, запретитель,<br/>тест-сканер или анализатор"]
|
||||
clean["шаг 3: формулировка удалена из конвенций,<br/>строка — в conventions/README.md"]
|
||||
clean["шаг 3: формулировка удалена из конвенций,<br/>строка — в перечень механизированного"]
|
||||
|
||||
f --> cond
|
||||
cond -->|нет| no
|
||||
@@ -85,8 +85,9 @@ flowchart TD
|
||||
- из файла конвенций убирается формулировка правила; остаётся, если нужно, одна
|
||||
строка «проверяется линтером `<имя>`» — но только там, где без неё раздел
|
||||
теряет связность;
|
||||
- правило переезжает в **перечень механизированного в
|
||||
`docs/conventions/README.md`** — со ссылкой на место механизации: конфиг
|
||||
- правило переезжает в **перечень механизированного в доме конвенций**
|
||||
(`docs/conventions/README.md` у каталога, отдельный раздел
|
||||
`docs/conventions.md` у файла) — со ссылкой на место механизации: конфиг
|
||||
линтера, собственный анализатор, тест-сканер исходников. Не названное место
|
||||
означает, что проход будет добросовестно проверять уже проверенное;
|
||||
- из контекста инструмента спек убирается дубль, если он там был.
|
||||
+2
-3
@@ -1,7 +1,7 @@
|
||||
# Журнал дефектов
|
||||
|
||||
Артефакт проекта, а не плагина: файл живёт в репозитории — **`docs/review.md`**,
|
||||
слот канона `av-dev-pm`. Здесь описано, зачем он и какой формы, потому что без
|
||||
слот канона `av-dev-docs`. Здесь описано, зачем он и какой формы, потому что без
|
||||
него конвейер не учится: находки закрываются, а почему их не поймали — забывается,
|
||||
и один и тот же класс проскакивает второй раз.
|
||||
|
||||
@@ -41,8 +41,7 @@
|
||||
## Форма записи
|
||||
|
||||
**Это дом формы, и у него есть копия.** Скелет `docs/review.md`, который кладёт
|
||||
в проект `av-dev-pm` (`skills/canon/references/skeletons.md`), повторяет её
|
||||
дословно — он уезжает в репозиторий и обязан там что-то говорить. Правка формы
|
||||
в проект `av-dev-docs:canon`, повторяет её дословно — он уезжает в репозиторий и обязан там что-то говорить. Правка формы
|
||||
здесь **обязана** тянуть правку скелета и запись в журнал версий канона; иначе
|
||||
проекты продолжат писать по старой форме, а конвейер — ждать поля, которого нет.
|
||||
Дословность сверяет `scripts/copies.py` маркетплейса по маркерам ниже — но
|
||||
+4
-4
@@ -94,9 +94,9 @@
|
||||
костяк — гейт, спеки, код, триаж**. Разрезать задачу, обе половины которой
|
||||
остаются в одной метке, значит заплатить костяк дважды за ту же проверку.
|
||||
Резать стоит там, где разрез **снимает доказательство с большей части диффа**.
|
||||
Шов и правило нарезки живут у того, кто ведёт задачи, — скилл `av-dev-pm:tasks`,
|
||||
его `references/split.md`. Пути туда конвейер не выносит: за пределы своего
|
||||
плагина он ходит вызовом скилла, а не файлом.
|
||||
Шов и правило нарезки живут у того, кто ведёт задачи, — скилл
|
||||
`av-dev-tasks:tasks`, его раздел о нарезке. Пути туда конвейер не выносит: за
|
||||
пределы своего плагина он ходит вызовом скилла, а не файлом.
|
||||
|
||||
Разметка в костяк не входит — она платится один раз на задачу, а не один раз на
|
||||
прогон, и потому **разрез задачи её не удваивает**. Это единственное, что стало
|
||||
@@ -156,7 +156,7 @@
|
||||
дёшев.
|
||||
|
||||
Считается это по журналу дефектов и по отчётам, а не по ощущению: метка
|
||||
напечатана в каждом отчёте, и посчитать её за спринт — работа на минуту.
|
||||
напечатана в каждом отчёте, и посчитать её за месяц — работа на минуту.
|
||||
|
||||
**У дрейфа вниз есть свой стимул, и его стоит назвать.** `small` дешевле по
|
||||
времени и по деньгам, а выбирает метку хоть и не автор, но проход, читающий
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "av-dev-docs",
|
||||
"description": "Документация проекта: канон раскладки (CLAUDE.md плюс docs/ — паспорт, архитектура, схема БД, безопасность, конвенции, разведка, ADR, журнал ревью), роли документов и правило единственного дома. Три операции одной машиной сравнения — check, adopt, upgrade — со скриптом docs.py; заведение нового проекта интервью по брифу; ведение содержимого по ходу разработки: синк после сделанной задачи, ADR промоутом из архивного design.md, записка разведки, запись дефекта в журнал ревью. Смысловое, чего скрипт не видит, судит скилл healthcheck двумя агентами разом — doc-consistency (документы между собой и с openspec) и doc-code-drift (факты против кода); язык документов вычитывает агент doc-wording. Задач не ведёт — это плагин av-dev-tasks, и он опционален.",
|
||||
"author": {
|
||||
"name": "Anton Vakhrushev",
|
||||
"email": "anwinged@gmail.com"
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: doc-code-drift
|
||||
description: "Сверка документов канона с кодом по закрытому перечню проверяемых фактов: имя основной ветки и команды из CLAUDE.md, запреты с путями, testdata и временный каталог, путь миграций из .pm.json, внешние зависимости поимённо в architecture.md против манифеста, настройки с числовым значением в database.md против конфига и кода, единые точки проекта против реального числа реализаций, capability против существующих модулей. Отвечает на «этот факт ещё верен», а не «эта архитектура правильная». Читает весь репозиторий, гоняет только читающие команды. Отдаёт готовые формулировки и ничего не правит сам. Согласованность документов между собой смотрит агент doc-consistency. Использовать на сессии между спринтами, а также после приведения проекта к канону (adopt) и после повышения версии канона (upgrade). Только чтение."
|
||||
description: "Сверка документов канона с кодом по закрытому перечню проверяемых фактов: имя основной ветки и команды из CLAUDE.md, запреты с путями, testdata и временный каталог, путь миграций из .pm.json, внешние зависимости поимённо в architecture.md против манифеста, настройки с числовым значением в database.md против конфига и кода, единые точки проекта против реального числа реализаций, capability против существующих модулей. Отвечает на «этот факт ещё верен», а не «эта архитектура правильная». Читает весь репозиторий, гоняет только читающие команды. Отдаёт готовые формулировки и ничего не правит сам. Согласованность документов между собой смотрит агент doc-consistency. Зовётся скиллом av-dev-docs:healthcheck — на весь канон разом; он же зовётся шагом adopt и шагом upgrade. Только чтение."
|
||||
tools: Read, Grep, Glob, Bash
|
||||
model: sonnet
|
||||
color: green
|
||||
@@ -50,7 +50,7 @@ color: green
|
||||
проверить, — это **не находка, а строка в границах покрытия**.
|
||||
|
||||
1. **Имя основной ветки** (`CLAUDE.md`). От неё считается база диффа
|
||||
(`git merge-base HEAD <ветка>`), в неё вливает батч, от неё ветвятся задачи.
|
||||
(`git merge-base HEAD <ветка>`), в неё коммитит работу конвейер.
|
||||
Проверка: `git symbolic-ref refs/remotes/origin/HEAD` либо перечень веток.
|
||||
Угадывание между `master` и `main` ломает интеграцию целиком, и это самая
|
||||
дешёвая находка из всех.
|
||||
@@ -117,7 +117,8 @@ color: green
|
||||
домах, противоречие между документами, поведение в обзоре, ADR и провенанс.
|
||||
Увидел — строкой в границы покрытия, находкой не оформляй.
|
||||
|
||||
**Язык** — у `doc-wording`. **Форму записи задач** — у `task-form`.
|
||||
**Язык документов** — у `doc-wording`, **язык записей задач** — у
|
||||
`task-wording`. **Форму записи задач** — у `task-form`.
|
||||
|
||||
**Машинной проверке — вообще ничего.** Всё, что ловят `docs.py check` и
|
||||
`tasks.py check` (пути канона, имена файлов, битые ссылки, версия, плейсхолдеры,
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: doc-consistency
|
||||
description: "Сверка документов канона между собой и с openspec: один факт, живущий в двух домах, прямое противоречие между документами (периметр, зависимости, обратимость), поведение системы, осевшее в architecture.md вместо спек, capability без обзора или с пересказом требований, число без провенанса в research, ADR без ссылки на архивный design.md и без парного статуса при замене, заглушка вместо честной строки в пустом слоте. Читает docs/ и openspec/, кода не читает. Отдаёт готовые формулировки и ничего не правит сам. Соответствие документов коду смотрит агент doc-code-drift, язык — doc-wording. Использовать на сессии между спринтами, а также после приведения проекта к канону (adopt) и после повышения версии канона (upgrade), на весь канон разом; на отдельной задаче не звать. Только чтение."
|
||||
description: "Сверка документов канона между собой и с openspec: один факт, живущий в двух домах, прямое противоречие между документами (периметр, зависимости, обратимость), поведение системы, осевшее в architecture.md вместо спек, capability без обзора или с пересказом требований, число без провенанса в research, ADR без ссылки на архивный design.md и без парного статуса при замене, заглушка вместо честной строки в пустом слоте. Читает docs/ и openspec/, кода не читает. Отдаёт готовые формулировки и ничего не правит сам. Соответствие документов коду смотрит агент doc-code-drift, язык — doc-wording. Зовётся скиллом av-dev-docs:healthcheck — на весь канон разом; он же зовётся шагом adopt и шагом upgrade. На отдельной задаче и на синке документации не звать. Только чтение."
|
||||
tools: Read, Grep, Glob
|
||||
model: opus
|
||||
color: yellow
|
||||
@@ -15,25 +15,25 @@ color: yellow
|
||||
машина, а что человек», и её правая колонка — твой устав дословно.
|
||||
|
||||
Карта домов, по которой ты судишь о правиле 1, — дословная копия канона; дом её
|
||||
`av-dev-pm/skills/canon/references/canon.md`, раздел «Правило единственного
|
||||
`av-dev-docs/skills/canon/references/canon.md`, раздел «Правило единственного
|
||||
дома», и правится она там. Здесь она стоит потому, что ты работаешь в
|
||||
репозитории проекта, где плагина может не быть вовсе.
|
||||
|
||||
<!-- копия: карта-домов из av-dev-pm/skills/canon/references/canon.md -->
|
||||
<!-- копия: карта-домов из av-dev-docs/skills/canon/references/canon.md -->
|
||||
| Факт | Дом |
|
||||
| --- | --- |
|
||||
| поведение системы | `openspec/specs/<capability>/spec.md` |
|
||||
| почему решено так | `adr/`, источник — архивный `design.md` |
|
||||
| граница домена, «чем не является» | `passport.md` |
|
||||
| инвариант и его severity | `CLAUDE.md` |
|
||||
| что приложение умеет и чего не умеет; порядок работ | `docs/tasks/ROADMAP.md` |
|
||||
| что приложение умеет и чего не умеет; порядок работ | `tasks/ROADMAP.md` |
|
||||
| измеренное число | `research/` |
|
||||
| настройка с числовым значением | `database.md` |
|
||||
| периметр и модель угроз | `security.md` |
|
||||
| что необратимо | `CLAUDE.md` — **не** `architecture.md` |
|
||||
| единые точки проекта | `architecture.md` |
|
||||
| имя основной ветки, `testdata`, временный каталог | `CLAUDE.md` |
|
||||
| что уже механизировано правилом | `conventions/README.md` |
|
||||
| что уже механизировано правилом | `conventions.*`, раздел «Механизировано» |
|
||||
<!-- /копия: карта-домов -->
|
||||
|
||||
**Факта нет в карте — дома у него нет**, и это находка о самом каноне, а не о
|
||||
@@ -45,8 +45,10 @@ color: yellow
|
||||
|
||||
## Что тебе дают
|
||||
|
||||
Корень проекта. Твоё чтение — `docs/**` (кроме `docs/tasks/`, его ведёт
|
||||
`tasks.py`), `CLAUDE.md`, `openspec/specs/**` и `openspec/config.yaml`. Плюс
|
||||
Корень проекта. Твоё чтение — `docs/**`, `CLAUDE.md`, `openspec/specs/**` и
|
||||
`openspec/config.yaml`. **Каталог задач не твой** — он лежит в `tasks/` (или в
|
||||
`docs/tasks/` на непереехавшем проекте), принадлежит другому плагину и ведётся
|
||||
своим скриптом; не открывай его ни в той форме, ни в другой. Плюс
|
||||
`openspec/changes/archive/`, когда проверяешь ADR: там лежат `design.md`, из
|
||||
которых записи промоутятся.
|
||||
|
||||
@@ -178,6 +180,14 @@ color: yellow
|
||||
|
||||
## Доклад
|
||||
|
||||
**Форма параллельна дому `вычитка-доклад` (`shared/language.md`), но копией не
|
||||
является, и маркера здесь нет намеренно.** Копию того дома везут проходы вычитки
|
||||
— `doc-wording` и `task-wording`; у судьи утверждений расходится каждое поле:
|
||||
находка стоит на **паре** документов, а не на одном, несёт **дом по канону** и не
|
||||
несёт «почему», а границы покрытия считают документы и спрашивают про спеки и
|
||||
архив изменений, а не про термины. Одинаков только порядок разделов, и сверять
|
||||
машиной в нём нечего.
|
||||
|
||||
Находки по одной, в порядке важности: прямые противоречия → факт в двух домах →
|
||||
поведение в обзоре → ADR и провенанс → пустые слоты. Первые ломают решения,
|
||||
которые по документам принимают; последние — только цену чтения.
|
||||
@@ -0,0 +1,242 @@
|
||||
---
|
||||
name: doc-wording
|
||||
description: "Вычитка языка документов проекта по информационному стилю — паспорт, архитектура, конвенции, безопасность, решения ADR, записки разведки, CLAUDE.md. Смотрит отглагольные существительные и страдательный залог, оценку без факта, стоп-слова и канцелярит, «одна мысль — одно предложение», англицизм при живом русском слове, жаргон и метафоры вместо прямого называния, термин, которого нет в документах проекта, транслит в имени файла. Отдаёт готовые формулировки на замену и ничего не правит сам. Записи каталога задач вычитывает отдельный агент task-wording, их форму — task-form. Зовётся по названной пачке правленных документов, а не на весь канон: последним шагом синка документации (av-dev-docs:docs), шагом заведения проекта (av-dev-docs:init), шагами adopt и upgrade скилла av-dev-docs:canon. Скилл healthcheck его не зовёт — там сверка утверждений, а не языка. Только чтение."
|
||||
tools: Read, Grep, Glob
|
||||
model: sonnet
|
||||
color: green
|
||||
---
|
||||
|
||||
Ты — **вычитка языка документов проекта**: паспорта, архитектуры, конвенций,
|
||||
модели угроз, решений ADR, записок разведки, `CLAUDE.md`. Оптика — слова и
|
||||
фразы, а не то, что текст описывает: ты не судишь, верно ли решение, полна ли
|
||||
архитектура и согласованы ли документы между собой.
|
||||
|
||||
Границу держи твёрдо. **Записи каталога задач — не твои**: их язык вычитывает
|
||||
`task-wording`, их форму — `task-form`. Открыл файл задачи по ссылке из
|
||||
документа и увидел язык — скажи одной строкой в конце доклада, не находкой. Две
|
||||
проверки одного места расходятся и начинают спорить.
|
||||
|
||||
Ты **ничего не правишь**. Каждая находка — готовая формулировка на замену,
|
||||
которую зовущий впишет сам. Файлы ты только читаешь.
|
||||
|
||||
## Что тебе дают
|
||||
|
||||
Список файлов или каталог: документы канона (`docs/*.md`), конвенции
|
||||
(`docs/conventions/`), решения (`docs/adr/`), записки (`docs/research/`),
|
||||
`CLAUDE.md` — вперемешку тоже.
|
||||
|
||||
По этим же документам проверяется, **известен ли термин**. Дали неполный набор —
|
||||
считай известными только те слова, что встречаются в поданных файлах, и говори
|
||||
об этом в границах покрытия.
|
||||
|
||||
## Правила
|
||||
|
||||
Дом — `shared/language.md` в репозитории плагинов, и там же объяснено, зачем
|
||||
стиль вообще нужен. Здесь только то, что нужно тебе для работы.
|
||||
|
||||
<!-- копия: язык-правила из shared/language.md -->
|
||||
|
||||
У каждого правила названа причина: она же говорит, где правило **не**
|
||||
применяется.
|
||||
|
||||
1. **Глагол вместо отглагольного существительного, активный залог.** «Обработчик
|
||||
не проверяет владельца», а не «проверка владельца не осуществляется»;
|
||||
«скрипт переписывает индекс», а не «индекс переписывается скриптом».
|
||||
Отглагольное существительное прячет того, кто действует, — а в техническом
|
||||
тексте важен именно он. Страдательный залог **остаётся**, когда деятель
|
||||
неизвестен или неважен: «файл удаляется» верно, если удаляет любая из трёх
|
||||
команд.
|
||||
|
||||
2. **Факт вместо оценки.** «Время ответа доходит до 800 мс», а не «работает
|
||||
медленно»; «тело 40 МиБ держит блокировку 5 секунд», а не «большие тела
|
||||
тормозят». Оценка допустима, когда факт стоит рядом, в той же фразе. Без
|
||||
факта это настроение, а не сведение, — и находка тем ценнее, что оценку
|
||||
потом не проверить.
|
||||
|
||||
3. **Стоп-слова.** Канцелярит (является, осуществляется, в целях, в рамках,
|
||||
данный, вышеуказанный), вводные-паразиты (в общем, как известно, стоит
|
||||
отметить), усилители (очень, крайне, достаточно, абсолютно, максимально),
|
||||
синонимы одного качества («понятный и простой»), неопределённое
|
||||
(соответствующий, определённый, некоторый).
|
||||
|
||||
Проверка одна: **вычеркни слово — смысл изменился, оставляй.** И осторожно с
|
||||
вводными: «если», «иначе», «при таком-то условии», «в отличие от» несут
|
||||
условие и противопоставление, то есть сведения, — их не трогают.
|
||||
|
||||
4. **Одна мысль — одно предложение.** Предложение с двумя независимыми
|
||||
утверждениями делится. **Причинную связь не режут**: «поэтому», «иначе», «раз
|
||||
так» — смысл, а не длина; рубленые фразы ради краткости тут вредят.
|
||||
|
||||
**Поля меты не делятся.** «Зачем» в мете задачи по формату — одно
|
||||
предложение: оно повторяется строкой индекса, и второму там не поместиться.
|
||||
Тесно — сокращают, но не делят. То же с любым полем вида `- **Имя:** …`.
|
||||
|
||||
5. **Англицизм, у которого есть живое русское слово, заменяется.**
|
||||
|
||||
| Калька | Русский аналог |
|
||||
| --- | --- |
|
||||
| флоу | поток, процесс, сценарий |
|
||||
| фикс, зафиксить | исправление, исправить, починить |
|
||||
| чекать | проверять |
|
||||
| апрув, заапрувить | согласование, согласовать |
|
||||
| best-effort | по возможности |
|
||||
| кейс | случай, сценарий |
|
||||
| перформанс | производительность |
|
||||
| матчинг, смэтчить | сопоставление, сопоставить |
|
||||
| зарелизить | выпустить, выложить |
|
||||
| отрефакторить | переписать, разделить, убрать второй путь |
|
||||
|
||||
Насильно не переводится то, что является **именем вещи**: термины технологий
|
||||
и протоколов (`SQL`, `API`, `CSV`, `N+1`, `IDOR`), имена классов, методов,
|
||||
полей, таблиц и команд, слаг, а также термин, у которого нет точного русского
|
||||
эквивалента и который в команде уже прижился.
|
||||
|
||||
Цель — простой и точный текст, а не пуризм. Русский аналог звучит коряво или
|
||||
искажает смысл — остаётся термин.
|
||||
|
||||
6. **Слово из своего словаря не трогается — список закрыт.** Оговорка «термин
|
||||
прижился» без списка проверяема на глаз и потому не проверяема: прижившимся
|
||||
выглядит любое слово, встреченное трижды.
|
||||
|
||||
| Термин | Что называет |
|
||||
| --- | --- |
|
||||
| интейк | заведение записи с фильтром и дедупом: «заведение» называет создание файла, слить их — смешать две операции |
|
||||
| триаж | стадия конвейера, сводящая находки в решение |
|
||||
| провенанс | обязательное свойство числа: чем и при каких условиях получено. «Источник» рядом называет саму запись, а не свойство |
|
||||
| дедуп, дедупликация | сверка нового против уже лежащего |
|
||||
| чек-лист | перечень, по которому идут сверху вниз, называя исход каждой строки |
|
||||
| дифф, `--base` | разница между состояниями в git |
|
||||
| промпт | текст, которым зовут модель |
|
||||
| change, capability, spec | сущности OpenSpec, имена вещей чужого инструмента |
|
||||
| generative, applicative | роды проходов ревью, вводятся определением по месту |
|
||||
|
||||
**Список закрыт.** Слово не отсюда и не из таблицы имён вещей выше — находка,
|
||||
а не «принятый стиль»: у него либо есть живой русский аналог, либо оно
|
||||
требует ввода одной строкой при первом употреблении.
|
||||
|
||||
Отсюда же читается снятое. Эти слова из текстов убраны, и возвращать их не
|
||||
надо: **конфляция** (смешение), **декорреляция** (разведённость, разведён с
|
||||
кем-то), **непоймание** (почему не поймали), **эвал-сет** (проверочный
|
||||
набор), **гайд** (руководство). Каждое было латинизмом или калькой при живом
|
||||
русском слове, и каждое к моменту снятия жило в трёх-шести файлах разом — то
|
||||
есть выглядело словарём, не будучи им.
|
||||
|
||||
7. **Жаргон и метафоры заменяются прямым называнием.** Автору образ понятен,
|
||||
читателю — нет.
|
||||
|
||||
| Метафора-жаргон | Прямо |
|
||||
| --- | --- |
|
||||
| рычаг (кэша, отбора) | условие отбора, параметр |
|
||||
| навешен не на тот счётчик | завязан не на тот счётчик |
|
||||
| переширокий матчинг по имени | слишком грубое сопоставление по имени, слишком много слабых совпадений |
|
||||
| костыль | временное решение, обходной путь — и в чём именно |
|
||||
| просело, отвалилось | стало медленнее на столько-то, перестало отвечать |
|
||||
|
||||
Проверка: **фраза требует, чтобы читатель додумал образ, — заменяется
|
||||
буквальным описанием того, что происходит.**
|
||||
|
||||
8. **Термин, которого нет в документах проекта, вводится одной строкой или не
|
||||
употребляется.** Термин, не встречающийся ни в паспорте, ни в архитектуре, ни
|
||||
в конвенциях, — свой словарь у отдельной записи, а это самый дешёвый способ
|
||||
сделать беклог нечитаемым для того, кто вернётся к нему через квартал.
|
||||
Заменять незнакомый термин догадкой нельзя: догадка о предметной области
|
||||
дороже непонятного слова, потому что выглядит понятной.
|
||||
|
||||
**Слово, занятое в другом смысле, — то же нарушение.** Термин, который в
|
||||
одном документе проекта значит одно, а здесь другое, ломает оба.
|
||||
|
||||
9. **Имя файла — английское слово по сути, а не транслит.** `queue-as-table`, а
|
||||
не `ochered-tablicej`; `move-parse-strict`, а не `razbor-hoda`. Транслит
|
||||
нечитаем тому, кто ищет по смыслу, и не сокращается, а имя стоит в ссылках,
|
||||
коммитах и путях, которые набирают руками. Переименование — **перенос ссылок
|
||||
одним проходом**, а не правка одного файла.
|
||||
|
||||
<!-- /копия: язык-правила -->
|
||||
|
||||
### Что из этих правил докладывается особым образом
|
||||
|
||||
**Правило 8, неизвестный термин.** Своей догадки не подставляй — ты не знаешь
|
||||
предметную область. Пиши «термин «X» не встречается ни в паспорте, ни в
|
||||
архитектуре, ни в конвенциях — введи строкой или назови известным словом».
|
||||
Слово, занятое в другом смысле, — та же находка, и в ней **называются оба
|
||||
места**: один документ канона, противоречащий другому словарём, ломает оба.
|
||||
|
||||
**Правило 9, имя файла.** Кириллицу в имени, не-kebab-case и форму имени ADR
|
||||
ловит `docs.py` — про них молчи. Твоё — **транслит**, потому что машина
|
||||
проверяет его эвристикой и ловит не всё: `sostoyanie-partii` проходит мимо неё.
|
||||
Чаще всего он заводится в `docs/adr/` и `docs/research/`, где имя придумывают на
|
||||
ходу. Находка — готовое английское имя на замену плюс напоминание про перенос
|
||||
ссылок одним проходом.
|
||||
|
||||
## Чего ты не проверяешь
|
||||
|
||||
Не своё бывает двух разных родов, и поступают с ними по-разному.
|
||||
|
||||
**Чужому подрядчику — строкой в границах покрытия.** Согласованность документов
|
||||
между собой (факт в двух домах, противоречие, поведение, осевшее в обзоре, ADR
|
||||
без ссылки, число без провенанса) — у `doc-consistency`; соответствие документов
|
||||
коду — у `doc-code-drift`; язык записей каталога задач — у `task-wording`, их
|
||||
форма — у `task-form`. Увидел — назови в конце одной строкой, чтобы находка не
|
||||
пропала, но находкой не оформляй.
|
||||
|
||||
**Машинной проверке — вообще ничего.** Всё, что ловит `docs.py check` (пути
|
||||
канона, файлы вне канона, имена файлов, битые ссылки, версия канона, нетронутые
|
||||
плейсхолдеры, маркеры долга) и что ловит `openspec.py check` скилла
|
||||
`av-dev-code:openspec` (форма `openspec/config.yaml`), **не пиши даже
|
||||
строкой**: это не потерянная находка, а уже проверенное. Повторять машинную
|
||||
проверку словами — заводить второй дом для одного правила.
|
||||
|
||||
**Содержание**: верно ли решение, разумен ли инвариант, полна ли архитектура.
|
||||
Это разбор, а не вычитка, — и о нём тоже молчи.
|
||||
|
||||
**Полезное действие, параллельность и работающий заголовок** — тоже не твои.
|
||||
Они в доктрине языка, судит их человек: находка по ним требует увидеть текст
|
||||
целиком, а не фразу.
|
||||
|
||||
## Порог вмешательства
|
||||
|
||||
<!-- копия: порог-правки из shared/language.md -->
|
||||
|
||||
**Правка без нарушенного правила не делается.** Текст, переписанный «чтобы
|
||||
звучало лучше», обесценивает список замечаний: когда половина из них вкусовая,
|
||||
перестают читать весь список, и вместе с ним пропадают настоящие находки.
|
||||
Сомневаешься — не правь. Формулировка, которая просто **не твоя**, — не находка.
|
||||
|
||||
**Систематичность нарушения — не довод в его пользу.** Одна и та же ошибка в
|
||||
пяти файлах не становится «принятым стилем»: чаще это значит, что правило не
|
||||
применялось вовсе, — и находка тем важнее. «Так сделано везде» годится как
|
||||
основание для **одной находки на весь набор** («правило N нарушено в пяти
|
||||
записях, перечень: …»), но не как основание промолчать. Принятым считается
|
||||
только то, что назвал зовущий или что записано в конвенциях проекта.
|
||||
|
||||
<!-- /копия: порог-правки -->
|
||||
|
||||
Один документ может дать несколько находок, но каждое место правится один раз:
|
||||
не предлагай два варианта на выбор, предлагай лучший.
|
||||
|
||||
## Доклад
|
||||
|
||||
<!-- копия: вычитка-доклад из shared/language.md -->
|
||||
|
||||
Находки по одной, в порядке важности: залог и оценки → жаргон и англицизмы →
|
||||
стоп-слова. Первые меняют, **что** читатель понимает; последние — только сколько
|
||||
он на это тратит.
|
||||
|
||||
```
|
||||
<файл>
|
||||
правило: <номер и короткое имя>
|
||||
сейчас: <как написано>
|
||||
предложение: <готовая формулировка, подставляемая как есть>
|
||||
почему: <одна фраза>
|
||||
```
|
||||
|
||||
В конце — **границы покрытия**: сколько файлов просмотрено из скольких, какие не
|
||||
смотрел и почему, и по чему проверялись термины (документы проекта названы или
|
||||
нет). Отчёт без этой строки читается как «всё вычитано», не сообщая, какая часть
|
||||
осталась нетронутой. Туда же — строка «замечено не по моей части»; машинно
|
||||
проверяемое в неё **не идёт**.
|
||||
|
||||
Ничего не нашёл — так и скажи одной строкой. Пустой доклад с границами покрытия
|
||||
полезнее выдуманной находки.
|
||||
|
||||
<!-- /копия: вычитка-доклад -->
|
||||
@@ -24,7 +24,9 @@ description: Привести проект к канону документов
|
||||
информационный стиль, применённый к проектным текстам, таблицы англицизмов и
|
||||
жаргона. Раскладка отвечает, где текст лежит, — этот файл отвечает, каким он
|
||||
должен быть. Правила общие для документов канона, задач, решений ADR и
|
||||
записок разведки; вычитывает их отдельным проходом агент `doc-wording`.
|
||||
записок разведки, и дом у них общий — `shared/language.md` в репозитории
|
||||
плагинов, а этот файл его копия. Вычитывают их два прохода по охвату:
|
||||
документы — `doc-wording`, записи каталога задач — `task-wording`.
|
||||
- [references/changelog.md](references/changelog.md) — журнал версий канона.
|
||||
|
||||
## Три правила, из которых всё следует
|
||||
@@ -47,18 +49,13 @@ ds="$CLAUDE_PLUGIN_ROOT/skills/canon/scripts/docs.py"
|
||||
|
||||
python3 $ds check --dir <корень> [--base <rev>] # раскладка, ссылки, версия, сверки
|
||||
python3 $ds version --dir <корень> # версия канона скрипта и проекта
|
||||
python3 $ds openspec-form # форма config.yaml против живого OpenSpec
|
||||
```
|
||||
|
||||
**`openspec-form` зовут не на каждом прогоне, а когда о нём попросил `check`.**
|
||||
Форма `openspec/config.yaml` описана в каноне слепком чужого инструмента — имя
|
||||
схемы и перечень артефактов, — и слепок стареет молча: OpenSpec переименует
|
||||
артефакт, правила под старым именем перестанут применяться, а конфиг останется
|
||||
выглядеть написанным. Поэтому `check` каждым прогоном сравнивает `major.minor`
|
||||
установленного OpenSpec с тем, на котором форма сверялась, и при расхождении
|
||||
даёт замечание с этой командой. Команда ничего не правит: она спрашивает
|
||||
инструмент и печатает, что разошлось. **Чинится это в плагине, а не в проекте** —
|
||||
константы `docs.py`, скелет в `skeletons.md` и запись в журнал версий канона.
|
||||
**Формы `openspec/config.yaml` здесь больше нет.** Каталог принадлежит конвейеру,
|
||||
и форму смотрит его скрипт — скилл `av-dev-code:openspec`, команда
|
||||
`openspec.py check`. Проект работает по OpenSpec, а плагина конвейера нет — форму
|
||||
не проверяет никто, и это надо сказать строкой доклада, а не считать, что она
|
||||
верна.
|
||||
|
||||
**Коды выхода — тот же словарь, что у `tasks.py`:** 0 сошлось, 1 дрейф, 2 ошибка
|
||||
употребления, 3 окружение, 4 внутренний сбой. Ветвись на коде, а не на тексте.
|
||||
@@ -90,18 +87,53 @@ capability: незаполненный канон это переходное с
|
||||
формулировка казалась удачной при написании. Ни один из них ничего не правит —
|
||||
оба возвращают готовые формулировки, подставляешь ты.
|
||||
|
||||
## Обращение к соседним плагинам
|
||||
|
||||
`adopt` зовёт двоих: `av-dev-code:openspec` (шаг 4, пункт 3) и
|
||||
`av-dev-tasks:tasks` (шаг 4, пункт 5). Каталоги `openspec/` и `tasks/` каноном не
|
||||
ведутся, и трогать их этому скиллу нечем, кроме вызова.
|
||||
|
||||
**Копия.** Дом правила — `shared/plugin-boundary.md` в репозитории плагинов.
|
||||
Правится дом, а не этот файл.
|
||||
|
||||
<!-- копия: граница-плагинов из shared/plugin-boundary.md -->
|
||||
|
||||
Плагины `av-dev` ставятся порознь, и ни один не вправе считать, что сосед на
|
||||
месте.
|
||||
|
||||
**Чужой скилл зовётся полным именем** — `av-dev-docs:canon`, `av-dev-tasks:tasks`,
|
||||
`av-dev-code:review`. Короткое имя может разрешиться в устаревшую проектную копию
|
||||
из `.claude/skills/`, и подмены не будет видно ни в докладе, ни в поведении.
|
||||
|
||||
**Путь в дерево чужого плагина не пишется никогда.** `$CLAUDE_PLUGIN_ROOT` ведёт
|
||||
только в свой плагин; вычисленный от него путь к соседу либо не откроется, либо
|
||||
откроет чужую установку. Нужен чужой справочник — зови владеющий им скилл, он
|
||||
прочитает его сам.
|
||||
|
||||
**Вызов не разрешился — плагина в проекте нет.** Это исход, а не поломка: назови
|
||||
строкой доклада, чего теперь не делает никто, и продолжай работу. Молчать нельзя,
|
||||
пропуск неотличим от сделанного; выдумывать обходной путь нельзя тоже.
|
||||
|
||||
**Присутствие узнаётся вызовом или следом в проекте, но не объявлением.** Перечня
|
||||
установленных плагинов проект не ведёт — он разошёлся бы с действительностью
|
||||
молча. Что сосед здесь работал, видно по заведённому им файлу: `docs/.pm.json` —
|
||||
канон, `<каталог задач>/.tasks.json` — задачи, `openspec/config.yaml` — конвейер.
|
||||
|
||||
<!-- /копия: граница-плагинов -->
|
||||
|
||||
Чем оборачивается отсутствие каждого — на самих пунктах шага 4. `adopt` из-за
|
||||
этого не останавливается ни в одном из двух случаев.
|
||||
|
||||
## `check`
|
||||
|
||||
1. `docs.py check`, при наличии базы диффа — с `--base`.
|
||||
2. **Агентов на каждом `check` не зови.** Оба — `doc-consistency` и
|
||||
`doc-code-drift` — зовутся раз в спринт (шаг сессии), а также шагом 6 `adopt`
|
||||
и шагом 6 `upgrade`, на весь канон разом. Они дороги: `doc-consistency` — тем,
|
||||
что на `opus` (сличение утверждений это суждение), `doc-code-drift` — тем, что
|
||||
читает репозиторий целиком, хотя сам идёт на `sonnet`. Позвал
|
||||
`doc-code-drift` — передай ему раздел запретов `CLAUDE.md`.
|
||||
3. Доклад: вывод скрипта строкой исхода, находки агентов поимённо, **граница
|
||||
покрытия** — что смотрели и чего не смотрели, и **кого из двоих позвал**:
|
||||
доклад, умолчавший об этом, читается как «сверено».
|
||||
2. **Судей документов на каждом `check` не зови.** Ими владеет отдельный скилл —
|
||||
`av-dev-docs:healthcheck`, — и там же записано, когда его звать: он дорог, и
|
||||
прогон по каждому `check` не окупается. `check` отвечает на «сходится ли
|
||||
форма», `healthcheck` — на «не разошлись ли утверждения».
|
||||
3. Доклад: вывод скрипта строкой исхода и **граница покрытия** — что смотрели и
|
||||
чего не смотрели. Если суждение здесь нужно, скажи это строкой и предложи
|
||||
`healthcheck`, а не зови агентов сам.
|
||||
|
||||
Дрейф раскладки чинится переносом; смысловые находки — это либо правка
|
||||
документа, либо задача, если работы больше чем на абзац.
|
||||
@@ -147,13 +179,14 @@ capability), `openspec/config.yaml`.
|
||||
1. `docs/.pm.json` с `{"canon": <текущая версия>}` и путём миграций, если БД есть;
|
||||
2. каталоги канона и скелет **по [references/skeletons.md](references/skeletons.md)**:
|
||||
незаполненное — одной честной информативной строкой, а не «TBD»;
|
||||
3. **OpenSpec, если его нет** — `openspec init --tools claude`, и `config.yaml`
|
||||
по тому же скелету. Каталог есть, а `config.yaml` из коробки — тот же случай,
|
||||
что отсутствие: закомментированный пример выглядит настройкой и не является
|
||||
ею. Пересказ инвариантов, конвенций и правил ревью из `context` вычисти
|
||||
ссылкой на дом — на переводимом проекте он там почти наверняка есть;
|
||||
3. **OpenSpec, если его нет или `config.yaml` остался примером** — **вызови
|
||||
Skill `av-dev-code:openspec`**. Каталог принадлежит конвейеру, и команда
|
||||
заведения с формой файла живут там. Пересказ инвариантов, конвенций и правил
|
||||
ревью из `context` вычисти ссылкой на дом — на переводимом проекте он там
|
||||
почти наверняка есть. Вызов не разрешился — `docs.py` о каталоге тогда тоже
|
||||
молчит, и форму `config.yaml` не проверяет никто; скажи это строкой;
|
||||
4. переносы содержимого;
|
||||
5. каталог задач — **вызови скилл `av-dev-pm:tasks`**, сценарий адаптации: он
|
||||
5. каталог задач — **вызови скилл `av-dev-tasks:tasks`**, сценарий адаптации: он
|
||||
владеет форматом задач. Он же переименует транслитные слаги в английские и
|
||||
тем же проходом починит перекрёстные ссылки;
|
||||
6. починка ссылок на перенесённое во всём репозитории — `docs/`, `openspec/`,
|
||||
@@ -164,16 +197,30 @@ capability), `openspec/config.yaml`.
|
||||
меняла `Taskfile`; шаг обязан **краснеть внятно**, если скрипт не найден, а не
|
||||
пропускаться. Передай ему базу диффа (`--base`) той же переменной, что и
|
||||
остальным шагам гейта: без неё сверка миграций со схемой не гоняется вовсе.
|
||||
Пример строки покажи человеку — гейт принадлежит проекту, и правит его он;
|
||||
Пример строки покажи человеку — гейт принадлежит проекту, и правит его он.
|
||||
|
||||
**Шагов в гейте три, и они независимы.** `docs.py check` не тянет за собой
|
||||
ни задачи, ни конвейер: без своих строк дрейф каталога задач и формы
|
||||
`openspec/config.yaml` перестаёт ловиться совсем. Ставь соседские шаги по
|
||||
следу присутствия — `<каталог задач>/.tasks.json` есть, значит ставится
|
||||
`tasks.py check --dir <каталог задач>`; `openspec/config.yaml` есть, значит
|
||||
ставится `openspec.py check`. Следа нет — плагина в проекте нет, шаг не
|
||||
ставится, и это **строка доклада**, а не поломка: назови, чего теперь не
|
||||
проверяет никто. У каждого шага своя переменная пути с тем же умолчанием на
|
||||
канонический путь маркетплейса; `$CLAUDE_PLUGIN_ROOT` в гейт не подставляй —
|
||||
он ведёт только в свой плагин;
|
||||
9. `docs.py check` — до **отсутствия дрейфа раскладки**. Замечания
|
||||
(незаполненные плейсхолдеры, слабое упоминание capability) остаются:
|
||||
незаполненный канон это объявленное переходное состояние из шага 5, а не
|
||||
отказ. **Пункт «задачи без цели» из вложенной проверки `tasks.py` тоже
|
||||
остаётся** и зелёным на этом шаге не станет: цели не сочиняются адаптацией
|
||||
(запрет в [tasks/references/adopt.md](../tasks/references/adopt.md)), их
|
||||
проставляет человек порциями переоценки на первой сессии. Пересчитай эти
|
||||
пункты в докладе переходного состояния — не выдавай их за поломку и не
|
||||
молчи о них.
|
||||
отказ. Пересчитай эти пункты в докладе переходного состояния — не выдавай
|
||||
их за поломку и не молчи о них.
|
||||
|
||||
**Задачи `docs.py` не проверяет** — их ведёт другой плагин, и согласованность
|
||||
каталога показывает только `tasks.py check`. Позвал на шаге 5 скилл задач —
|
||||
его отчёт идёт в доклад отдельной строкой, и пункт «задачи без цели» в нём
|
||||
зелёным не станет: цели не сочиняются адаптацией (запрет записан у того, кто
|
||||
ведёт задачи), их проставляет человек порциями переоценки на первом груминге —
|
||||
скилл `av-dev-tasks:groom`.
|
||||
|
||||
### 5. Объяви переходное состояние
|
||||
|
||||
@@ -192,12 +239,24 @@ capability), `openspec/config.yaml`.
|
||||
проекте обычно самый урожайный — правило единственного дома до адаптации никто не
|
||||
проверял.
|
||||
|
||||
Зови **`doc-consistency`** (документы между собой и с openspec) и
|
||||
**`doc-code-drift`** (факты против кода). Разбирай порциями, а не одним заходом.
|
||||
Вызови Skill **`av-dev-docs:healthcheck`** — он зовёт обоих судей на весь канон
|
||||
разом и держит разбор урожая порциями.
|
||||
|
||||
**Передай им объявленное переходное состояние из шага 5** — иначе честная строка
|
||||
в незаполненном слоте вернётся находкой, а это не поломка, а объявленный долг.
|
||||
|
||||
### 7. Вычитай написанное — агент `doc-wording`
|
||||
|
||||
Судьи смотрят утверждения, а `adopt` только что **писал текст**: честные строки
|
||||
в пустые слоты, переписанные при переносе абзацы, шапки перенесённых документов.
|
||||
Язык этого текста не проверяет никто другой, а зовущий здесь по определению тот,
|
||||
кто его и написал.
|
||||
|
||||
Позови агента **по названной пачке** — документы, которые ты завёл или правил,
|
||||
плюс перенесённые целиком. Весь канон ему не нужен: он работает по списку, и
|
||||
список же служит ему словарём терминов. Находки — готовые формулировки,
|
||||
подставляешь их ты.
|
||||
|
||||
## `upgrade` — канон вырос
|
||||
|
||||
1. `docs.py version` — версия проекта и версия скрипта.
|
||||
@@ -208,7 +267,11 @@ capability), `openspec/config.yaml`.
|
||||
применяются по порядку.
|
||||
4. Подними `canon` в `docs/.pm.json` до текущей.
|
||||
5. `docs.py check`.
|
||||
6. **Позови обоих судей** — `doc-consistency` и `doc-code-drift`.
|
||||
6. **Позови судей** — Skill `av-dev-docs:healthcheck`.
|
||||
7. **Позови вычитку** — агент `doc-wording`, но **только по тем документам,
|
||||
которых записи журнала коснулись**, и только если правка была текстовой, а не
|
||||
переименованием файла. Записи журнала пишутся руками в проектной прозе, и
|
||||
дописанный по журналу раздел — такой же свежий текст, как на синке.
|
||||
|
||||
Записи журнала описывают **что сделать проекту**. Если запись этого не говорит —
|
||||
это дефект журнала, и о нём надо сказать, а не догадываться.
|
||||
+87
-97
@@ -1,6 +1,6 @@
|
||||
# Канон документов проекта
|
||||
|
||||
**Версия 7.**
|
||||
**Версия 12.**
|
||||
|
||||
Это **единственный дом определения канона**. Скиллы `init`, `canon` и `docs`
|
||||
читают его, а не пересказывают: три описания одной раскладки разъедутся, и
|
||||
@@ -24,7 +24,13 @@
|
||||
|
||||
## Сопровождение и эксплуатация — целое и часть
|
||||
|
||||
Одна тема живёт в трёх местах канона, и путать их слова нельзя.
|
||||
**Копия.** Дом — `shared/operations.md` в репозитории плагинов: словарь
|
||||
делят роадмап, архитектура и тема ревью `operations`, то есть три плагина,
|
||||
и ни один из трёх им не владеет. Правится дом, а не этот файл.
|
||||
|
||||
<!-- копия: сопровождение-словарь из shared/operations.md -->
|
||||
|
||||
Одна тема живёт в трёх местах, и путать их слова нельзя.
|
||||
|
||||
**Сопровождение** — всё, чем держат проект: инструмент и сборка, процесс,
|
||||
выкладка, метрики и логи, инфраструктура, дежурство. **Эксплуатация** — его
|
||||
@@ -45,6 +51,8 @@
|
||||
экране» — сопровождение: наблюдаем мы. Одни и те же метрики попадают в разные
|
||||
секции роадмапа, и это верно — секции отвечают на разные вопросы.
|
||||
|
||||
<!-- /копия: сопровождение-словарь -->
|
||||
|
||||
## Раскладка
|
||||
|
||||
**Документ канона живёт файлом или каталогом.** `docs/security.md` и
|
||||
@@ -67,8 +75,8 @@ docs/
|
||||
adr.md | adr/ почему решено так; статусы, правило замены
|
||||
review.md | review/ настройка конвейера под проект + журнал дефектов
|
||||
<своя тема>.md | <своя тема>/ всё, что проект счёл нужным проверять
|
||||
tasks/ скилл tasks: items/, ROADMAP.md, BACKLOG.md,
|
||||
SPRINT.md, REJECTED.md
|
||||
tasks/ каталог задач — плагин av-dev-tasks, не канон;
|
||||
лежит в корне, вне docs/, и канон его не требует
|
||||
openspec/
|
||||
config.yaml только нужды генерации артефактов + ссылки
|
||||
specs/<capability>/spec.md что система делает — нормативно
|
||||
@@ -106,8 +114,8 @@ openspec/
|
||||
| `database.*` | источник | `operations` — схема и настройки с числами |
|
||||
| `CLAUDE.md`, `AGENTS.md` | источник | `autotests` (семантика гейта); инварианты — сквозные |
|
||||
| `openspec/specs/` | источник | `requirements` |
|
||||
| `openspec/config.yaml` | процессный | — (настройка порождения артефактов, слой **до** тем) |
|
||||
| `tasks/` | процессный | — |
|
||||
| `openspec/config.yaml` | процессный | — (настройка порождения артефактов, слой **до** тем; заводит конвейер) |
|
||||
| `tasks/` | процессный | — (чужое владение: плагин `av-dev-tasks`) |
|
||||
| `review.*` | процессный | — (настройка самого конвейера, слой **над** темами) |
|
||||
| `adr.*` | процессный | — |
|
||||
| `research.*` | процессный | — |
|
||||
@@ -141,8 +149,8 @@ openspec/
|
||||
Цена этого решения записана, а не подразумевается: **расхождение изменения с
|
||||
записанным решением прогоном больше не ловится.** Раньше архитектурный проход
|
||||
читал `adr/` и мог сказать «здесь отменено решение ADR-2026-03-11, а парного
|
||||
статуса нет»; теперь это скажет только `doc-consistency` на сессии между
|
||||
спринтами. Сделка сознательная — ADR объясняет прошлое, а не предъявляет
|
||||
статуса нет»; теперь это скажет только `doc-consistency`, а зовёт его скилл
|
||||
`healthcheck`. Сделка сознательная — ADR объясняет прошлое, а не предъявляет
|
||||
требование к изменению, и чтение всего каталога решений на каждой задаче
|
||||
оплачивалось на каждой, а срабатывало на единицах.
|
||||
|
||||
@@ -176,7 +184,7 @@ kebab-case.** Причина не эстетическая: имя файла с
|
||||
какую тему питает. **Кто именно закрывает тему, здесь не указано намеренно**: это
|
||||
зависит от метки прогона и меняется вместе с конвейером, а документ живёт
|
||||
дольше. Раскладку «тема → проход → глубина» держит скилл
|
||||
`av-dev-pipeline:review-pipeline`.
|
||||
`av-dev-code:review`.
|
||||
|
||||
**Общего словаря у канона с конвейером ровно три вида имён: имена категорий,
|
||||
имена тем и имена меток.** Категорий три — `тема`, `источник`, `процессный`;
|
||||
@@ -345,9 +353,16 @@ kebab-case.** Причина не эстетическая: имя файла с
|
||||
|
||||
### `tasks/`
|
||||
|
||||
Раскладку, форму записи и команды держит скилл `tasks` — канон фиксирует имена
|
||||
файлов (`items/`, `ROADMAP.md`, `BACKLOG.md`, `SPRINT.md`, `REJECTED.md`) и то,
|
||||
от чего зависит, читается ли проект как продукт.
|
||||
**Каталог задач канону не принадлежит.** Его ведёт отдельный плагин
|
||||
`av-dev-tasks` — своим скриптом, своим конфигом `<каталог>/.tasks.json` и своей
|
||||
версией формата. Канон **резервирует место** в `docs/` и внутрь не смотрит:
|
||||
`docs.py` каталог не открывает, его отсутствия не считает дрейфом и согласованность
|
||||
задач не проверяет. Проект, поставивший только канон документов, задач не ведёт
|
||||
вовсе, и отказом это быть не может.
|
||||
|
||||
Раскладку, форму записи и команды держит скилл `av-dev-tasks:tasks`. Ниже — то,
|
||||
от чего зависит, читается ли проект как продукт: канон высказывается об этом
|
||||
потому, что роадмап отвечает на вопрос о **системе**, а не о работах.
|
||||
|
||||
**`ROADMAP.md` отвечает на «что приложение уже умеет и чего ещё не умеет».** Это
|
||||
не очередь работ: цель — **возможность приложения**, задача — шаг к ней.
|
||||
@@ -370,23 +385,25 @@ kebab-case.** Причина не эстетическая: имя файла с
|
||||
| 🔬 `research` | исход — знание, а не изменение |
|
||||
|
||||
**Схемы записи здесь нет намеренно.** Какие разделы тип требует, нужна ли ему
|
||||
цель и берётся ли он в спринт — скилл `tasks`: сводка в его
|
||||
[SKILL.md](../../tasks/SKILL.md), раздел «Тип записи», подробно — по файлу на
|
||||
тип в `tasks/references/task-<тип>.md`. Канон фиксирует **словарь**, потому что
|
||||
цель и берётся ли он в работу — скилл `av-dev-tasks:tasks`, раздел «Тип
|
||||
записи», подробно — по файлу на тип в его `references/task-<тип>.md`. Ссылки в
|
||||
дерево того плагина здесь нет намеренно: он ставится отдельно, и путь наружу
|
||||
разрешился бы не всегда. Канон фиксирует **словарь**, потому что
|
||||
от него зависит, читается ли проект как продукт; схема — механика ведения задач,
|
||||
и второй её экземпляр разошёлся бы с первым (он и разошёлся: канон успел
|
||||
объявить цель у `fix` запрещённой, хотя она там необязательна).
|
||||
|
||||
Схема требуется **к взятию в спринт**, а не к заведению: беклог пополняется чаще,
|
||||
Схема требуется **к взятию в работу**, а не к заведению: беклог пополняется чаще,
|
||||
чем разбирается, и требование на входе выгоняло бы в заметки то, что должно
|
||||
лежать задачей. Запись, не собравшая разделы своего типа, — законное состояние
|
||||
беклога; невзятой её делает `sprint take`.
|
||||
беклога; невзятой её делает `tasks.py ready`.
|
||||
|
||||
Отдельного типа для незаполненной записи нет: «ещё не описано» — состояние, а не
|
||||
род работы, и называется оно **`research` без раздела «Вопрос»**. Такая запись в
|
||||
спринт не берётся и лежит в конце своей категории.
|
||||
работу не берётся и лежит в конце своей категории.
|
||||
|
||||
Раскладку, форму записи и алгоритм работы над каждым типом держит скилл `tasks`.
|
||||
Раскладку, форму записи и алгоритм работы над каждым типом держит скилл
|
||||
`av-dev-tasks:tasks`.
|
||||
|
||||
### `CLAUDE.md`
|
||||
|
||||
@@ -399,66 +416,37 @@ kebab-case.** Причина не эстетическая: имя файла с
|
||||
Плюс то, что нужно git-операциям и проходам и не выводится ниоткуда:
|
||||
|
||||
- **имя основной ветки** — от неё считается база диффа
|
||||
(`git merge-base HEAD <ветка>`), в неё вливает батч, от неё ветвятся задачи.
|
||||
(`git merge-base HEAD <ветка>`), в неё коммитит работу конвейер.
|
||||
Угадывание между `master` и `main` ломает интеграцию целиком;
|
||||
- **что запускать запрещено, с путями** — рабочая БД, боевой каталог данных,
|
||||
внешние сервисы. Запретом с путями, а не «будь осторожен»;
|
||||
- **где `testdata`** и что в них лежит; **куда писать временное**;
|
||||
- **что считается необратимым** — единственный дом: от обратимости зависит вся
|
||||
шкала ранжирования триажа и право проходов на `critical`;
|
||||
- **общий станок**, врывающийся в замороженный спринт; **ориентир по размеру
|
||||
спринта**.
|
||||
- **что считается сломанным** — красная проверка, обгоняющая развитие;
|
||||
**ориентир по размеру порции**, если он замерялся. Оба слота читает скилл
|
||||
`av-dev-tasks:groom`, и имена их — его; названы они здесь потому, что дом
|
||||
содержимого `CLAUDE.md` один и он тут.
|
||||
|
||||
### `openspec/config.yaml`
|
||||
|
||||
**Только нужды генерации артефактов** — язык, правила именования capability,
|
||||
придирки валидатора RFC 2119 — плюс **адреса** документов канона. Правило ревью,
|
||||
пересказ конвенций и инварианты сюда не пишутся: у них есть свои дома, и второй
|
||||
дом разойдётся на первой же правке.
|
||||
**Файл канону не принадлежит, и проверяет его тоже не канон.** Каталог
|
||||
`openspec/` — предпосылка конвейера: без него не работают ни `opsx:propose`, ни
|
||||
ревью дизайна, ни сверка требований. Заводит его, настраивает и **проверяет
|
||||
форму** скилл `av-dev-code:openspec`: там образец файла, там же скрипт
|
||||
`openspec.py check`. `docs.py` о файле не говорит ничего.
|
||||
|
||||
**Каталог `openspec/` — часть канона, а не соседняя технология.** В нём дом темы
|
||||
`requirements`, и заводится он командой: `openspec init --tools claude`. Её
|
||||
выполняет `init` на новом проекте и `adopt` на переводимом; из канона она названа
|
||||
поимённо потому, что её печатает отказ `docs.py`, а отказ без команды заставляет
|
||||
искать её в другом месте.
|
||||
Канон называет его здесь по одной причине: `openspec/specs/` — **дом темы
|
||||
`requirements`**, и без этой строки карта тем неполна. На форму самого
|
||||
`config.yaml` канон не высказывается.
|
||||
|
||||
**Файл из коробки настройкой не является.** `openspec init` кладёт `config.yaml`,
|
||||
где и `context`, и `rules` лежат закомментированным примером. Такой файл читается
|
||||
как настроенный — он есть, он валиден, у него правильное имя, — а работает как
|
||||
пустой: предложение пишется без языка, без правил именования capability и без
|
||||
знания, где лежит граница домена. Это ровно тот класс, против которого написан
|
||||
весь канон, и потому здесь он проверяется машиной, а не чтением.
|
||||
|
||||
Проверяется пять вещей, и каждая — про молчащий пробел, а не про вкус:
|
||||
|
||||
1. **`openspec/` есть.** Нет — нет и дома темы `requirements`.
|
||||
2. **Имя файла `config.yaml`.** `config.yml` OpenSpec не читает и об этом не
|
||||
сообщает: настройка, написанная в файл с таким именем, пропадает целиком.
|
||||
3. **`context` и `rules.specs` не остались примером.** Правила для `specs`
|
||||
обязаны называть `SHALL`: требование без этого литерала валидатор отвергает.
|
||||
4. **`context` называет `passport` и `CLAUDE.md`.** Предложение пишется **до**
|
||||
того, как кто-либо откроет `docs/`; без этих двух адресов его пишут, не зная
|
||||
ни границы домена, ни инвариантов.
|
||||
5. **Ключи под `rules:` — имена артефактов схемы** (`proposal`, `specs`,
|
||||
`design`, `tasks`). Правило, адресованное несуществующему артефакту, не
|
||||
применяется и об этом молчит: `rules.spec` вместо `rules.specs` — конфиг,
|
||||
выглядящий написанным и не работающий.
|
||||
|
||||
**Схема и перечень артефактов — слепок чужого инструмента, и он стареет.**
|
||||
OpenSpec переименует артефакт или сменит схему — правила под прежним именем
|
||||
перестанут действовать молча, а канон будет продолжать требовать прежнее.
|
||||
Поэтому за свежестью слепка следит машина: `check` сравнивает `major.minor`
|
||||
установленного OpenSpec с версией, на которой форма сверялась, и при расхождении
|
||||
даёт **замечание** (не отказ: патч-версии формы не меняют, а нагоняй на каждый
|
||||
багфикс приучает пролистывать блок). Перепроверяет `docs.py openspec-form` — он
|
||||
спрашивает сам инструмент и печатает, что разошлось. **Чинится это в плагине, а
|
||||
не в проекте:** константы скрипта, скелет и запись в журнал версий канона.
|
||||
|
||||
Шестого — «нет ли здесь пересказа» — машина не проверяет: отличить ссылку от
|
||||
пересказа она не умеет. Это работа `doc-consistency`, и раздел «Что проверяет
|
||||
машина, а что человек» называет её строкой.
|
||||
|
||||
Форма — [skeletons.md](skeletons.md).
|
||||
**Одно за каноном всё же остаётся, и это не форма, а единственный дом.** Блок
|
||||
`context` — самое частое место для второго дома: он читается при порождении
|
||||
каждого артефакта, туда удобно дописать «чтобы агент знал», и так заводятся копии
|
||||
инвариантов, конвенций, состава гейта и правил ревью. Расходятся они молча.
|
||||
Разрез: **утверждение, которое можно опровергнуть, открыв другой файл проекта, —
|
||||
пересказ; строка, которая говорит, какой файл открыть, — ссылка.** Машина этого
|
||||
не различает; судит агент `doc-consistency`, и `config.yaml` у него во входе.
|
||||
|
||||
## Правило единственного дома
|
||||
|
||||
@@ -471,14 +459,14 @@ OpenSpec переименует артефакт или сменит схему
|
||||
| почему решено так | `adr/`, источник — архивный `design.md` |
|
||||
| граница домена, «чем не является» | `passport.md` |
|
||||
| инвариант и его severity | `CLAUDE.md` |
|
||||
| что приложение умеет и чего не умеет; порядок работ | `docs/tasks/ROADMAP.md` |
|
||||
| что приложение умеет и чего не умеет; порядок работ | `tasks/ROADMAP.md` |
|
||||
| измеренное число | `research/` |
|
||||
| настройка с числовым значением | `database.md` |
|
||||
| периметр и модель угроз | `security.md` |
|
||||
| что необратимо | `CLAUDE.md` — **не** `architecture.md` |
|
||||
| единые точки проекта | `architecture.md` |
|
||||
| имя основной ветки, `testdata`, временный каталог | `CLAUDE.md` |
|
||||
| что уже механизировано правилом | `conventions/README.md` |
|
||||
| что уже механизировано правилом | `conventions.*`, раздел «Механизировано» |
|
||||
<!-- /дом: карта-домов -->
|
||||
|
||||
## Пустое называется пустым
|
||||
@@ -506,9 +494,9 @@ OpenSpec переименует артефакт или сменит схему
|
||||
| `docs/review-brief.md` | документы канона и есть бриф; остаток — в `review.md` |
|
||||
| `docs/specs/` | `openspec/specs/` (поведение) и `architecture.md` (обзор) |
|
||||
| `docs/drafts/` | идея → запись `research`; отказ → ADR; порядок → `ROADMAP.md`; размышление → `opsx:explore` |
|
||||
| `docs/plan.md` | `docs/tasks/ROADMAP.md` |
|
||||
| `docs/plan.md` | `tasks/ROADMAP.md` |
|
||||
| `BRIEF.md` | `passport.md` |
|
||||
| `docs/backlog/` | `docs/tasks/` |
|
||||
| `docs/backlog/` | `tasks/` в корне репозитория |
|
||||
| `docs/review-journal.md`, `docs/review/journal.md` | `docs/review.md` |
|
||||
|
||||
## Что проверяет машина, а что человек
|
||||
@@ -527,17 +515,25 @@ OpenSpec переименует артефакт или сменит схему
|
||||
| маркеры долга — числом | **протухший факт, разошедшийся с кодом** | `doc-code-drift` |
|
||||
| миграция изменена, а `database.md` нет | зависимость в манифесте, не названная в обзоре | `doc-code-drift` |
|
||||
| capability без упоминания в `architecture.md` | второй способ там, где обзор обещал единственный | `doc-code-drift` |
|
||||
| `openspec/config.yaml`: имя, `schema`, незаменённый пример, адреса паспорта и `CLAUDE.md`, ключи `rules` против артефактов схемы | **пересказ документа канона в `context` вместо ссылки** | `doc-consistency` |
|
||||
| версия OpenSpec разошлась с той, на которой сверена форма `config.yaml` | придирки валидатора: сменились ли они | никакой — проявляются отказом `openspec validate --strict` |
|
||||
| | **пересказ документа канона в `context` вместо ссылки** | `doc-consistency` |
|
||||
| | придирки валидатора: сменились ли они | никакой — проявляются отказом `openspec validate --strict` |
|
||||
| | связность и читаемость | `doc-wording` |
|
||||
|
||||
**Форма `openspec/config.yaml` в левой колонке отсутствует не по забывчивости.**
|
||||
С канона 10 `docs.py` о файле не говорит ничего: имя, `schema`, незаменённый
|
||||
пример, адреса паспорта и `CLAUDE.md`, ключи `rules` и сторож версии OpenSpec —
|
||||
всё это смотрит `openspec.py check` скилла `av-dev-code:openspec`. Плагина
|
||||
конвейера в проекте может не быть; тогда форму не проверяет никто, и это строка
|
||||
доклада.
|
||||
|
||||
**Агентов двое, и разведены они по глубине, а не по охвату.** `doc-consistency`
|
||||
читает только `docs/` и `openspec/`, `doc-code-drift` — весь репозиторий и гоняет
|
||||
читающие команды. Слитый агент делал бы одну половину поверхностной; тот же
|
||||
разрез, что между `task-form` и `doc-wording`.
|
||||
разрез, что между `task-form` и `task-wording`.
|
||||
|
||||
**Зовутся оба одинаково — раз в спринт на сессии, а также после `adopt` и после
|
||||
`upgrade`, на весь канон разом.** Не на синке документации: `doc-consistency` на
|
||||
**Зовутся оба одинаково и одним скиллом — `av-dev-docs:healthcheck`, на весь
|
||||
канон разом; шагом `adopt` и шагом `upgrade` его зовёт `canon`.** Не на синке
|
||||
документации: `doc-consistency` на
|
||||
`opus` по каждой сделанной задаче не окупается, а расхождение между двумя документами по
|
||||
определению требует двух, и на большинстве задач синк правит один. Пачка,
|
||||
отбираемая работой, вдобавок не видит того, чего работа не касалась, — а именно
|
||||
@@ -554,30 +550,24 @@ OpenSpec переименует артефакт или сменит схему
|
||||
|
||||
```json
|
||||
{
|
||||
"canon": 7,
|
||||
"migrations": "internal/store/migrations",
|
||||
"tasks": {
|
||||
"backlog": "INDEX.md"
|
||||
}
|
||||
"canon": <текущая версия>,
|
||||
"migrations": "internal/store/migrations"
|
||||
}
|
||||
```
|
||||
|
||||
`canon` — версия канона, под которую проект приведён, целым числом: обратной
|
||||
совместимости у канона нет, есть «приведён» и «не приведён». `migrations` — путь
|
||||
каталога миграций, если БД есть; по нему `docs.py` делает сверку с
|
||||
`database.md`. `tasks` — настройки каталога задач, переехавшие сюда из прежнего
|
||||
`<tasks>/.tasks.json`: **один конфиг на весь канон, а не по одному на каталог**.
|
||||
Внутри `tasks` — **только имена файлов и заголовков** (`items`, `backlog`,
|
||||
`roadmap`, `sprint`, `rejected`, `sprint_section`, `oracle_word` и заголовки
|
||||
разделов тела: `criteria_heading`, `surface_heading`, `questions_heading`,
|
||||
`completion_heading`, `repro_heading`, `question_heading`, `answer_heading`,
|
||||
`scope_heading`), и ключ пишется, лишь когда имя отличается от умолчания.
|
||||
**Словаря типов здесь нет** — он закрыт каноном, а не настраивается проектом:
|
||||
настраиваемый словарь типов разъехался бы на синонимах ровно так же, как
|
||||
открытый. **Категорий беклога здесь тоже нет:** их дом — заголовки `##` самого
|
||||
индекса, и второй список сразу разошёлся бы с первым. Неизвестный ключ `tasks.py`
|
||||
отвергает кодом 3, поэтому лишнее слово в этом объекте останавливает работу с
|
||||
задачами целиком.
|
||||
совместимости у канона нет, есть «приведён» и «не приведён». Число подставляет
|
||||
`init`, `adopt` или `upgrade`, и берётся оно из `docs.py version`, а не из
|
||||
образца: литерал в образце протухает на первом же повышении канона.
|
||||
`migrations` — путь каталога миграций, если БД есть; по нему `docs.py` делает
|
||||
сверку с `database.md`.
|
||||
|
||||
**Ключа `tasks` здесь больше нет.** Настройки каталога задач вернулись в свой
|
||||
файл `<каталог задач>/.tasks.json`, потому что ведёт их другой плагин: конфиг,
|
||||
лежащий в `docs/`, был бы домом, которого нет у проекта, поставившего учёт работ
|
||||
без канона документов. Состав ключей описывает тот плагин, а не канон. Прежний
|
||||
ключ читается, пока живы непереехавшие проекты, и `tasks.py` говорит о нём
|
||||
замечанием на каждом прогоне — версия 8 журнала просит его убрать.
|
||||
|
||||
Ключей будет больше по мере роста проверок; неизвестный ключ `docs.py`
|
||||
игнорирует, отсутствующий — считает «проверка неприменима» и говорит об этом
|
||||
+187
-1
@@ -13,6 +13,192 @@ upgrade` идёт по записям снизу вверх от версии п
|
||||
|
||||
---
|
||||
|
||||
## Версия 12 — 2026-08-09
|
||||
|
||||
Спринты отменены. Работа идёт задача за задачей, и замороженный набор перестал
|
||||
что-либо удерживать: он отвечал на вопрос «что делать дальше», а между наборами
|
||||
на этот вопрос не отвечал никто.
|
||||
|
||||
**Что изменилось.** Индексов задач два вместо трёх: `SPRINT.md` упразднён.
|
||||
Приоритет стал тем, чем он и является, — **порядком строк в `BACKLOG.md`**:
|
||||
первая строка секции это то, что делают следующим. Назначает порядок человек,
|
||||
машина его не выводит; двигают его `move --after` и `move --first` с причиной.
|
||||
|
||||
Гейт готовности записи стоял на взятии задачи в спринт — единственном месте, где
|
||||
её судили целиком. Момент нужен и без спринта: теперь это команда
|
||||
`tasks.py ready <слаг>`, и зовёт её тот, кто берёт задачу в работу.
|
||||
|
||||
Ритуал между спринтами (`av-dev-tasks:session`) стал скиллом груминга
|
||||
(`av-dev-tasks:groom`): два вопроса — что сейчас самое важное и что перестало
|
||||
быть важным.
|
||||
|
||||
**Что сделать проекту.**
|
||||
|
||||
1. **Вернуть задачи из набора в беклог и снести `SPRINT.md`.** Порядок такой:
|
||||
`git rm tasks/SPRINT.md`, затем `tasks.py check --dir tasks --fix`. Строки
|
||||
набора после удаления файла становятся бездомными, и `--fix` возвращает их в
|
||||
беклог **в конец своей секции** — с пометкой, что позицию назначает человек.
|
||||
Наоборот делать нельзя: `check` без удалённого файла увидит третий индекс и
|
||||
станет ругаться на него, а не чинить.
|
||||
2. **Снять теги `sprint:<слаг>`** с записей — `tasks.py edit <слаг> --rm-tag
|
||||
sprint:<слаг>`. Тег больше никем не читается, а `check` о нём молчит: он
|
||||
законный свободный тег. Пропущенный вреда не сделает, но и пользы не несёт.
|
||||
3. **Расставить порядок** — первый груминг: `av-dev-tasks:groom`. После шага 1
|
||||
очередь состоит из того, что машина поставила в конец, то есть очереди нет
|
||||
вовсе. Пока порядок не назначен, «что делать дальше» по-прежнему без ответа.
|
||||
4. Поправить упоминания спринта в `CLAUDE.md` проекта, если они были: слот
|
||||
«общий станок» переехал в груминг под именем «что считается сломанным»,
|
||||
ориентир «5–8 задач в спринте» стал ориентиром размера порции разбора.
|
||||
5. `docs/.pm.json`: `"canon": 12`.
|
||||
|
||||
**Чего делать не надо.** `REJECTED.md`, `ROADMAP.md` и файлы `items/` не
|
||||
меняются: спринт жил только в собственном индексе и в тегах.
|
||||
|
||||
---
|
||||
|
||||
## Версия 11 — 2026-08-09
|
||||
|
||||
Каталог задач уехал из `docs/` в корень репозитория. Версия 8 отпустила его из
|
||||
канона — перестала требовать, перестала проверять, — но место он занимал всё то
|
||||
же, `docs/tasks/`. Полдела: каталог, принадлежащий одному плагину, лежал внутри
|
||||
дерева, которым владеет другой. Проекту, поставившему учёт работ без канона
|
||||
документов, приходилось заводить `docs/` ради одной вложенной папки.
|
||||
|
||||
**Что изменилось.** Дом задач — `tasks/` в корне репозитория. `tasks.py` ищет его
|
||||
там первым; `docs/tasks/` и `doc/tasks/` остаются в списке поиска для
|
||||
непереехавших проектов, а `init` заводит только в корне. Настройки — там же,
|
||||
`tasks/.tasks.json`.
|
||||
|
||||
**Что осталось терпимым.** `docs.py` по-прежнему не считает `docs/tasks/` файлом
|
||||
вне канона: непереехавший проект не должен получать выдуманную ошибку вдобавок к
|
||||
этой записи, которая и так велит ему переехать.
|
||||
|
||||
**Что сделать проекту.**
|
||||
|
||||
1. `git mv docs/tasks tasks` — одним коммитом вместе с шагом 2, чтобы ссылки не
|
||||
жили битыми между коммитами.
|
||||
2. **Починить относительные ссылки внутри записей.** Файл `tasks/items/x.md`
|
||||
стал на уровень ближе к корню: `../../passport.md` в теле записи теперь
|
||||
`../docs/passport.md`. Тот же сдвиг у ссылок из индексов. Это самая тихая
|
||||
часть переезда: битая относительная ссылка не мешает `tasks.py check`, её
|
||||
ловит только `docs.py check` и только у документов канона.
|
||||
3. Проверить ссылки **на** задачи снаружи: `CLAUDE.md`, `README.md`, гейт,
|
||||
`docs/review.md`. Путь `docs/tasks/...` в них теперь ведёт в никуда.
|
||||
4. Поправить путь в гейте: `tasks.py check --dir tasks`.
|
||||
5. `docs/.pm.json`: `"canon": 11`.
|
||||
|
||||
## Версия 10 — 2026-08-09
|
||||
|
||||
Проверка формы `config.yaml` ушла к тому, кто файл заводит. Версия 9 перенесла в
|
||||
конвейер настройку OpenSpec и честно назвала остаток: форма и сторож версии
|
||||
остались в `docs.py`, то есть у файла было два плагина — один заводит, другой
|
||||
проверяет. Остаток закрыт.
|
||||
|
||||
**Что появилось.** Скрипт `openspec.py` в скилле `av-dev-code:openspec`, две
|
||||
команды: `check --dir <корень>` — форма в проекте, `form` — сверка слепка с живым
|
||||
OpenSpec. Коды выхода те же, что у `docs.py` и `tasks.py`.
|
||||
|
||||
**Что удалено из `docs.py`.** Константы `OPENSPEC_*`, проверка формы, сторож
|
||||
версии и подкоманда `openspec-form` — 252 строки. Скрипт канона про
|
||||
`openspec/config.yaml` не говорит теперь ничего; `openspec/specs/` он по-прежнему
|
||||
знает, потому что это дом темы `requirements` и часть карты тем.
|
||||
|
||||
**Что стало лучше по дороге.** Адреса `docs/passport.md` и `CLAUDE.md` требуются
|
||||
теперь **только к тем документам, которые в проекте есть**. Прежняя проверка
|
||||
требовала их безусловно, то есть на проекте без канона документов требовала
|
||||
битую ссылку. Теперь отсутствие документа — строка «не проверялось» с указанием,
|
||||
что без канона конвейер работает вслепую.
|
||||
|
||||
**Что осталось за каноном.** Один вопрос, и это не форма: не пересказан ли в
|
||||
`context` документ, у которого есть свой дом. Разрез — утверждение, опровергаемое
|
||||
открытием другого файла, против строки «открой такой-то файл»; машине он не
|
||||
виден, судит агент `doc-consistency`, и `config.yaml` у него во входе.
|
||||
|
||||
**Что сделать проекту.**
|
||||
|
||||
1. Заменить в гейте и в скриптах `docs.py openspec-form` на `openspec.py form`.
|
||||
Подкоманды больше нет: прежний вызов упадёт ошибкой употребления (код 2), а не
|
||||
промолчит.
|
||||
2. **Добавить в гейт шаг `openspec.py check`, если проект работает по OpenSpec.**
|
||||
Форму раньше проверял `docs.py check` заодно; теперь он о ней молчит, и без
|
||||
отдельного шага незаменённый пример в `config.yaml` перестанет ловиться. Это
|
||||
главная потеря этого повышения, и она тихая.
|
||||
3. Проект по OpenSpec без установленного `av-dev-code` — форму не проверяет
|
||||
никто. Либо поставить плагин, либо назвать это принятым риском вслух.
|
||||
4. `docs/.pm.json`: `"canon": 10`.
|
||||
|
||||
## Версия 9 — 2026-08-09
|
||||
|
||||
OpenSpec уехал в конвейер. Каталог `openspec/` версией 7 был объявлен слотом
|
||||
канона: `init` его заводил, `adopt` тоже, образец `config.yaml` лежал в скелетах,
|
||||
а отсутствие каталога `docs.py` считал отказом. Разрез был проведён не там. По
|
||||
OpenSpec работает конвейер — без каталога не запускаются ни `opsx:propose`, ни
|
||||
ревью дизайна, ни сверка требований, — а канон документов о нём только
|
||||
высказывался. Проект, которому конвейер не нужен, получал отказ за отсутствие
|
||||
того, чем не пользуется.
|
||||
|
||||
**Что появилось.** Скилл `av-dev-code:openspec`: заводит каталог, заменяет
|
||||
закомментированный пример в `config.yaml` настройкой, объясняет разрез между
|
||||
ссылкой и пересказом. Образец файла переехал туда же — в
|
||||
`references/config-skeleton.md` того скилла.
|
||||
|
||||
**Что изменилось.** `init` и `canon adopt` OpenSpec больше не заводят, а **зовут
|
||||
скилл конвейера**; вызов не разрешился — плагина конвейера нет, и это строка
|
||||
доклада, а не поломка. Отсутствие `openspec/` для `docs.py check` стало
|
||||
неприменимостью вместо отказа: остальные четыре проверки формы идут только при
|
||||
живом каталоге.
|
||||
|
||||
**Что осталось на месте и почему.** Проверка формы `config.yaml` и сторож версии
|
||||
(`docs.py openspec-form`) пока живут в скрипте канона — переносить их значит
|
||||
заводить в конвейере свой скрипт, а этого у него нет ни одного. Разрез названного
|
||||
это не отменяет, но и не завершает: **у файла сейчас два плагина — один заводит,
|
||||
другой проверяет**, и это временное состояние, а не задуманное.
|
||||
|
||||
**Что сделать проекту.**
|
||||
|
||||
1. Ничего не переносить: файлы проекта эта версия не двигает. Меняется только то,
|
||||
кто их заводит.
|
||||
2. Проверить, что плагин `av-dev-code` установлен, если проект работает по
|
||||
OpenSpec. Без него `docs.py check` про каталог промолчит — и молчание это
|
||||
законное, так что отсутствие настройки перестанет ловиться само.
|
||||
3. Проект **не** работает по OpenSpec: убедиться, что `openspec/` нет, и
|
||||
перестать держать его пустым ради проверки. Она больше не требует каталога.
|
||||
4. `docs/.pm.json`: `"canon": 9`.
|
||||
|
||||
## Версия 8 — 2026-08-09
|
||||
|
||||
Канон отпустил каталог задач. Плагин `av-dev-pm` расколот на `av-dev-docs`
|
||||
(документы) и `av-dev-tasks` (учёт работ), и каждый теперь ставится сам по себе.
|
||||
Пока владелец был один, `docs/tasks/` числился слотом канона: `docs.py` требовал
|
||||
каталог, звал внутрь чужой скрипт и выдавал его дрейф за свой, а настройки задач
|
||||
жили ключом `tasks` в `docs/.pm.json`. Для проекта, поставившего только документы,
|
||||
всё это — отказ на ровном месте: задач он не ведёт, и требовать их не за что.
|
||||
|
||||
**Что изменилось.** Каталог задач канону не принадлежит; канон резервирует ему
|
||||
место в `docs/` и внутрь не смотрит. `docs.py` больше не проверяет согласованность
|
||||
задач вовсе — это делает `tasks.py` сам, командой своего плагина. Дом настроек
|
||||
каталога задач — `<каталог задач>/.tasks.json`; ключ `tasks` в `docs/.pm.json`
|
||||
читается, только пока своего файла нет, и об этом говорится замечанием.
|
||||
|
||||
**Что удалено.** Проверка `check_tasks` из `docs.py` и ключ `"tasks"` из скелета
|
||||
`docs/.pm.json`.
|
||||
|
||||
**Что сделать проекту.**
|
||||
|
||||
1. Перенести настройки задач: содержимое ключа `"tasks"` из `docs/.pm.json` — в
|
||||
`docs/tasks/.tasks.json` тем же объектом. Ключа в проекте нет (имена файлов
|
||||
и заголовков умолчательные) — переносить нечего, шаг пропускается.
|
||||
2. Удалить ключ `"tasks"` из `docs/.pm.json` после переноса. Оставленный он не
|
||||
читается, и `tasks.py` скажет об этом замечанием на каждом прогоне.
|
||||
3. Проверить, что согласованность задач по-прежнему кто-то гоняет: раньше её
|
||||
тянул за собой `docs.py check`, теперь — только `tasks.py check`. **Если в
|
||||
гейте проекта стоял один `docs.py`, добавить туда второй шаг** — иначе дрейф
|
||||
индексов перестанет ловиться молча, и это самая вероятная потеря на этом
|
||||
повышении.
|
||||
4. Установить оба плагина, если нужны оба: `av-dev-docs` и `av-dev-tasks`
|
||||
вместо прежнего `av-dev-pm`. Прежний из `enabledPlugins` убрать.
|
||||
5. `docs/.pm.json`: `"canon": 8`.
|
||||
|
||||
## Версия 7 — 2026-08-07
|
||||
|
||||
`openspec/` был предпосылкой, о которой канон говорил, но за которой не следил.
|
||||
@@ -510,5 +696,5 @@ ADR объясняет прошлое решение, а не предъявля
|
||||
|
||||
| Что копируется | Дом определения |
|
||||
| --- | --- |
|
||||
| форма записи журнала дефектов в `docs/review.md` | `av-dev-pipeline/skills/review-pipeline/references/review-journal.md` |
|
||||
| форма записи журнала дефектов в `docs/review.md` | `av-dev-code/skills/review/references/review-journal.md` |
|
||||
| правило заведения ADR в `docs/adr/README.md` | [canon.md](canon.md), раздел `adr/` |
|
||||
@@ -0,0 +1,211 @@
|
||||
# Язык проектных текстов
|
||||
|
||||
**Копия.** Дом — `shared/language.md` в репозитории плагинов; язык общий для
|
||||
документов канона и для задач, и потому не принадлежит ни одному плагину.
|
||||
Правится дом, а не этот файл: расхождение ловит `copies.py` на гейте коммита.
|
||||
|
||||
<!-- копия: язык-доктрина из shared/language.md -->
|
||||
|
||||
Правила — для всего, что пишется словами: задачи и цели, документы канона,
|
||||
решения ADR, записки разведки, сообщения коммитов. Не для кода и не для
|
||||
сообщений программы пользователю — там свои конвенции проекта.
|
||||
|
||||
Основа — **информационный стиль** Максима Ильяхова ([учебник
|
||||
бюро](https://bureau.ru/projects/book-text/), книга «Пиши, сокращай»). Он
|
||||
написан для рекламы, статей и писем, поэтому взят не целиком.
|
||||
|
||||
## Зачем он здесь
|
||||
|
||||
Проектный текст читают в двух положениях, и оба неудобные: **выбирают, брать ли
|
||||
задачу**, глядя в строку индекса и один экран тела; и **возвращаются через
|
||||
квартал**, не помня контекста. Оба положения наказывают одно и то же — слова, не
|
||||
несущие сведений. Информационный стиль ровно про это, и его польза здесь не
|
||||
эстетическая: текст, из которого нельзя достать факт, заставляет открывать код,
|
||||
а это и есть цена, которой мы избегаем.
|
||||
|
||||
## Что взято сверх правил вычитки
|
||||
|
||||
Эти три требования судит человек, а не проход вычитки: находка по ним требует
|
||||
увидеть текст целиком, а не фразу.
|
||||
|
||||
**Полезное действие.** У каждого текста есть вопрос, на который он отвечает, и
|
||||
читатель, который этот вопрос задаёт. Не отвечает — не пишется. У задачи это
|
||||
«зачем она нужна» и «что станет наблюдаемо иначе»; у документа канона — его
|
||||
собственный вопрос («что это за система», «как сложено», «почему так решили»).
|
||||
Текст, который не отвечает ни на чей вопрос, сокращается до нуля — это законный
|
||||
исход правки.
|
||||
|
||||
**Параллельность.** Однородное пишется одинаково: пункты списка — одной
|
||||
грамматической формой, разделы одного вида — одним порядком, заголовки одного
|
||||
уровня — одним типом фразы. Разнобой читатель принимает за разницу по существу и
|
||||
ищет её.
|
||||
|
||||
**Заголовок работает.** Заголовок называет содержание раздела, а не тему
|
||||
вообще: «Что проверяет `check`», а не «О проверках». Заголовков ставится
|
||||
столько, чтобы длинный текст можно было просматривать, а не только читать
|
||||
подряд.
|
||||
|
||||
## Что отброшено намеренно
|
||||
|
||||
Инфостиль написан для текстов, где читателя надо удержать. Проектный текст
|
||||
читают потому, что надо, и держать его нечем. Отсюда три расхождения:
|
||||
|
||||
- **Парцелляция и рубленые фразы — нет.** Приём «Коротко. Ещё короче. Вот так»
|
||||
ломает причинную связь, а в решении и в задаче ценность именно в ней:
|
||||
«поэтому», «иначе», «раз так» несут смысл и остаются.
|
||||
- **Не всякое вводное — мусор.** «Если», «иначе», «при таком-то условии»,
|
||||
«в отличие от» — это условия и противопоставления, то есть сведения. Режутся
|
||||
вводные, которые не меняют смысл предложения.
|
||||
- **Скобки и точка с запятой остаются.** В технической записи скобки несут
|
||||
уточнение — имя команды, единицы, слаг, — и запрет на них удлинил бы текст, а
|
||||
не сократил. Запрет на многоточие принимаем: в проектном тексте оно значит
|
||||
«дописать позже», и такой текст лучше не публиковать.
|
||||
|
||||
И общее: инфостиль призывает «снять корону с себя и надеть на читателя». Здесь
|
||||
читатель — **ты сам через квартал** и тот, кто возьмёт задачу. Писать для них
|
||||
значит называть состояние и остаток, а не пересказывать, как было интересно
|
||||
разбираться.
|
||||
|
||||
<!-- /копия: язык-доктрина -->
|
||||
|
||||
## Правила
|
||||
|
||||
<!-- копия: язык-правила из shared/language.md -->
|
||||
|
||||
У каждого правила названа причина: она же говорит, где правило **не**
|
||||
применяется.
|
||||
|
||||
1. **Глагол вместо отглагольного существительного, активный залог.** «Обработчик
|
||||
не проверяет владельца», а не «проверка владельца не осуществляется»;
|
||||
«скрипт переписывает индекс», а не «индекс переписывается скриптом».
|
||||
Отглагольное существительное прячет того, кто действует, — а в техническом
|
||||
тексте важен именно он. Страдательный залог **остаётся**, когда деятель
|
||||
неизвестен или неважен: «файл удаляется» верно, если удаляет любая из трёх
|
||||
команд.
|
||||
|
||||
2. **Факт вместо оценки.** «Время ответа доходит до 800 мс», а не «работает
|
||||
медленно»; «тело 40 МиБ держит блокировку 5 секунд», а не «большие тела
|
||||
тормозят». Оценка допустима, когда факт стоит рядом, в той же фразе. Без
|
||||
факта это настроение, а не сведение, — и находка тем ценнее, что оценку
|
||||
потом не проверить.
|
||||
|
||||
3. **Стоп-слова.** Канцелярит (является, осуществляется, в целях, в рамках,
|
||||
данный, вышеуказанный), вводные-паразиты (в общем, как известно, стоит
|
||||
отметить), усилители (очень, крайне, достаточно, абсолютно, максимально),
|
||||
синонимы одного качества («понятный и простой»), неопределённое
|
||||
(соответствующий, определённый, некоторый).
|
||||
|
||||
Проверка одна: **вычеркни слово — смысл изменился, оставляй.** И осторожно с
|
||||
вводными: «если», «иначе», «при таком-то условии», «в отличие от» несут
|
||||
условие и противопоставление, то есть сведения, — их не трогают.
|
||||
|
||||
4. **Одна мысль — одно предложение.** Предложение с двумя независимыми
|
||||
утверждениями делится. **Причинную связь не режут**: «поэтому», «иначе», «раз
|
||||
так» — смысл, а не длина; рубленые фразы ради краткости тут вредят.
|
||||
|
||||
**Поля меты не делятся.** «Зачем» в мете задачи по формату — одно
|
||||
предложение: оно повторяется строкой индекса, и второму там не поместиться.
|
||||
Тесно — сокращают, но не делят. То же с любым полем вида `- **Имя:** …`.
|
||||
|
||||
5. **Англицизм, у которого есть живое русское слово, заменяется.**
|
||||
|
||||
| Калька | Русский аналог |
|
||||
| --- | --- |
|
||||
| флоу | поток, процесс, сценарий |
|
||||
| фикс, зафиксить | исправление, исправить, починить |
|
||||
| чекать | проверять |
|
||||
| апрув, заапрувить | согласование, согласовать |
|
||||
| best-effort | по возможности |
|
||||
| кейс | случай, сценарий |
|
||||
| перформанс | производительность |
|
||||
| матчинг, смэтчить | сопоставление, сопоставить |
|
||||
| зарелизить | выпустить, выложить |
|
||||
| отрефакторить | переписать, разделить, убрать второй путь |
|
||||
|
||||
Насильно не переводится то, что является **именем вещи**: термины технологий
|
||||
и протоколов (`SQL`, `API`, `CSV`, `N+1`, `IDOR`), имена классов, методов,
|
||||
полей, таблиц и команд, слаг, а также термин, у которого нет точного русского
|
||||
эквивалента и который в команде уже прижился.
|
||||
|
||||
Цель — простой и точный текст, а не пуризм. Русский аналог звучит коряво или
|
||||
искажает смысл — остаётся термин.
|
||||
|
||||
6. **Слово из своего словаря не трогается — список закрыт.** Оговорка «термин
|
||||
прижился» без списка проверяема на глаз и потому не проверяема: прижившимся
|
||||
выглядит любое слово, встреченное трижды.
|
||||
|
||||
| Термин | Что называет |
|
||||
| --- | --- |
|
||||
| интейк | заведение записи с фильтром и дедупом: «заведение» называет создание файла, слить их — смешать две операции |
|
||||
| триаж | стадия конвейера, сводящая находки в решение |
|
||||
| провенанс | обязательное свойство числа: чем и при каких условиях получено. «Источник» рядом называет саму запись, а не свойство |
|
||||
| дедуп, дедупликация | сверка нового против уже лежащего |
|
||||
| чек-лист | перечень, по которому идут сверху вниз, называя исход каждой строки |
|
||||
| дифф, `--base` | разница между состояниями в git |
|
||||
| промпт | текст, которым зовут модель |
|
||||
| change, capability, spec | сущности OpenSpec, имена вещей чужого инструмента |
|
||||
| generative, applicative | роды проходов ревью, вводятся определением по месту |
|
||||
|
||||
**Список закрыт.** Слово не отсюда и не из таблицы имён вещей выше — находка,
|
||||
а не «принятый стиль»: у него либо есть живой русский аналог, либо оно
|
||||
требует ввода одной строкой при первом употреблении.
|
||||
|
||||
Отсюда же читается снятое. Эти слова из текстов убраны, и возвращать их не
|
||||
надо: **конфляция** (смешение), **декорреляция** (разведённость, разведён с
|
||||
кем-то), **непоймание** (почему не поймали), **эвал-сет** (проверочный
|
||||
набор), **гайд** (руководство). Каждое было латинизмом или калькой при живом
|
||||
русском слове, и каждое к моменту снятия жило в трёх-шести файлах разом — то
|
||||
есть выглядело словарём, не будучи им.
|
||||
|
||||
7. **Жаргон и метафоры заменяются прямым называнием.** Автору образ понятен,
|
||||
читателю — нет.
|
||||
|
||||
| Метафора-жаргон | Прямо |
|
||||
| --- | --- |
|
||||
| рычаг (кэша, отбора) | условие отбора, параметр |
|
||||
| навешен не на тот счётчик | завязан не на тот счётчик |
|
||||
| переширокий матчинг по имени | слишком грубое сопоставление по имени, слишком много слабых совпадений |
|
||||
| костыль | временное решение, обходной путь — и в чём именно |
|
||||
| просело, отвалилось | стало медленнее на столько-то, перестало отвечать |
|
||||
|
||||
Проверка: **фраза требует, чтобы читатель додумал образ, — заменяется
|
||||
буквальным описанием того, что происходит.**
|
||||
|
||||
8. **Термин, которого нет в документах проекта, вводится одной строкой или не
|
||||
употребляется.** Термин, не встречающийся ни в паспорте, ни в архитектуре, ни
|
||||
в конвенциях, — свой словарь у отдельной записи, а это самый дешёвый способ
|
||||
сделать беклог нечитаемым для того, кто вернётся к нему через квартал.
|
||||
Заменять незнакомый термин догадкой нельзя: догадка о предметной области
|
||||
дороже непонятного слова, потому что выглядит понятной.
|
||||
|
||||
**Слово, занятое в другом смысле, — то же нарушение.** Термин, который в
|
||||
одном документе проекта значит одно, а здесь другое, ломает оба.
|
||||
|
||||
9. **Имя файла — английское слово по сути, а не транслит.** `queue-as-table`, а
|
||||
не `ochered-tablicej`; `move-parse-strict`, а не `razbor-hoda`. Транслит
|
||||
нечитаем тому, кто ищет по смыслу, и не сокращается, а имя стоит в ссылках,
|
||||
коммитах и путях, которые набирают руками. Переименование — **перенос ссылок
|
||||
одним проходом**, а не правка одного файла.
|
||||
|
||||
<!-- /копия: язык-правила -->
|
||||
|
||||
## Порог правки
|
||||
|
||||
<!-- копия: порог-правки из shared/language.md -->
|
||||
|
||||
**Правка без нарушенного правила не делается.** Текст, переписанный «чтобы
|
||||
звучало лучше», обесценивает список замечаний: когда половина из них вкусовая,
|
||||
перестают читать весь список, и вместе с ним пропадают настоящие находки.
|
||||
Сомневаешься — не правь. Формулировка, которая просто **не твоя**, — не находка.
|
||||
|
||||
**Систематичность нарушения — не довод в его пользу.** Одна и та же ошибка в
|
||||
пяти файлах не становится «принятым стилем»: чаще это значит, что правило не
|
||||
применялось вовсе, — и находка тем важнее. «Так сделано везде» годится как
|
||||
основание для **одной находки на весь набор** («правило N нарушено в пяти
|
||||
записях, перечень: …»), но не как основание промолчать. Принятым считается
|
||||
только то, что назвал зовущий или что записано в конвенциях проекта.
|
||||
|
||||
<!-- /копия: порог-правки -->
|
||||
|
||||
И обратное: язык правится **по ходу той операции, которая записи касается**.
|
||||
Беклог не переписывают ради языка.
|
||||
+35
-90
@@ -19,9 +19,12 @@
|
||||
`<!-- дом: <id> -->` … `<!-- /дом: <id> -->`, копия —
|
||||
`<!-- копия: <id> из <путь> -->` … `<!-- /копия: <id> -->`;
|
||||
`scripts/copies.py` маркетплейса требует дословного
|
||||
совпадения. Комментарии невидимы в отрендеренном markdown и уезжают в проект
|
||||
вместе со скелетом — там они говорят читателю, что у текста есть дом. Правишь
|
||||
текст внутри маркеров — правь дом, а не копию.
|
||||
совпадения. Правишь текст внутри маркеров — правь дом, а не копию.
|
||||
|
||||
**Сама пара маркеров в проект не переносится.** Это машинерия маркетплейса:
|
||||
путь в ней ведёт в дерево плагина, и в репозитории проекта он не разрешится ни
|
||||
во что. Кладя скелет, копируй содержимое между маркерами, а строки
|
||||
`<!-- копия: … -->` и `<!-- /копия: … -->` оставляй здесь.
|
||||
|
||||
## `docs/passport.md`
|
||||
|
||||
@@ -29,7 +32,7 @@
|
||||
# Паспорт проекта
|
||||
|
||||
Зачем это и для кого. [architecture.md](architecture.md) отвечает «как
|
||||
устроено», [tasks/ROADMAP.md](tasks/ROADMAP.md) — «в каком порядке», паспорт —
|
||||
устроено», [tasks/ROADMAP.md](../tasks/ROADMAP.md) — «в каком порядке», паспорт —
|
||||
«зачем и для кого».
|
||||
|
||||
## Цель
|
||||
@@ -205,7 +208,7 @@
|
||||
|
||||
Верно одно из трёх:
|
||||
|
||||
<!-- копия: adr-когда-заводить из av-dev-pm/skills/canon/references/canon.md -->
|
||||
<!-- копия: adr-когда-заводить из av-dev-docs/skills/canon/references/canon.md -->
|
||||
- **дорогой откат** — переделка стоит дороже переписывания одного файла;
|
||||
- **намеренный отказ** от очевидного подхода;
|
||||
- **пересмотр прежнего решения** — тогда у старой записи обязателен статус
|
||||
@@ -340,7 +343,7 @@
|
||||
|
||||
Форма:
|
||||
|
||||
<!-- копия: журнал-дефектов-форма из av-dev-pipeline/skills/review-pipeline/references/review-journal.md -->
|
||||
<!-- копия: журнал-дефектов-форма из av-dev-code/skills/review/references/review-journal.md -->
|
||||
## ГГГГ-ММ-ДД — <краткое последствие> [проскочил|пойман]
|
||||
|
||||
- **Где:** путь:строка либо «конвейер, а не код»
|
||||
@@ -354,6 +357,9 @@
|
||||
<!-- /копия: журнал-дефектов-форма -->
|
||||
```
|
||||
|
||||
Пара маркеров `копия:` внутри — машинерия маркетплейса; в `docs/review.md`
|
||||
проекта уезжает только содержимое между ними (см. выше).
|
||||
|
||||
Новый проект: «Дефектов пока не было. Настройка конвейера появится с первым
|
||||
ревью.»
|
||||
|
||||
@@ -401,9 +407,10 @@ severity стоит здесь, а не выводится каждым прох
|
||||
|
||||
- **Основная ветка:** <имя>
|
||||
- **Необратимое** (спрашивается у человека всегда):
|
||||
- **Общий станок** — какая проверка, покраснев, врывается в замороженный спринт:
|
||||
- **Ориентир по размеру спринта:** 5–8 задач, ориентир а не закон
|
||||
- **Что такое «сделана»:** пайплайн проекта пройден + критерии приёмки проверены
|
||||
- **Что считается сломанным** — какая красная проверка обгоняет развитие,
|
||||
то есть останавливает текущую работу:
|
||||
- **Ориентир по размеру порции:** своё число, если замерялось
|
||||
- **Что такое «сделана»:** конвейер проекта пройден + критерии приёмки проверены
|
||||
поимённо
|
||||
|
||||
## Язык
|
||||
@@ -419,93 +426,31 @@ severity стоит здесь, а не выводится каждым прох
|
||||
|
||||
## `openspec/config.yaml`
|
||||
|
||||
Каталог `openspec/` заводится командой — `openspec init --tools claude`, — и она
|
||||
кладёт `config.yaml` с закомментированным примером внутри. Пример **заменяется
|
||||
целиком**: нетронутый файл выглядит настроенным, а работает как пустой.
|
||||
**Образец переехал.** Файл заводит и заполняет плагин конвейера — скилл
|
||||
`av-dev-code:openspec`, — потому что по OpenSpec работает он, а не канон
|
||||
документов. Проект без конвейера каталога `openspec/` не имеет вовсе, и образец
|
||||
файла, которого у него нет, в скелетах канона лежал бы мёртвым грузом.
|
||||
|
||||
**Это маршрутизатор, а не второй дом фактов.** Сюда пишут ровно то, что нужно
|
||||
**в момент порождения артефакта** и чего в этот момент ещё никто не открыл:
|
||||
язык, правила именования capability, придирки валидатора и **адреса** документов
|
||||
канона. Пересказ паспорта, инвариантов, конвенций и правил ревью сюда не
|
||||
переносится: расходится он молча, а замечают это в уже написанном предложении.
|
||||
|
||||
```yaml
|
||||
schema: spec-driven
|
||||
|
||||
context: |
|
||||
Language: Russian
|
||||
Пиши на русском, но:
|
||||
- Структурные заголовки оставляй на английском:
|
||||
## ADDED/MODIFIED/REMOVED Requirements, ### Requirement:, #### Scenario:
|
||||
- Ключевые слова GIVEN/WHEN/THEN и RFC 2119 (SHALL/MUST/SHOULD) — на английском
|
||||
- Технические термины, пути и код — на английском
|
||||
|
||||
Имена capabilities:
|
||||
- Capability — это ПОВЕДЕНИЕ или домен системы, а не пакет кода (совпадение с
|
||||
именем пакета допустимо, но не критерий).
|
||||
- Существительное, понятное без знания кода: ingest, parsing, storage,
|
||||
read-api. НЕ store/httpapi — это реализация.
|
||||
- Гранулярность по принципу «требования меняются вместе». Дробить, когда в
|
||||
одной спеке смешиваются разные заботы. Переименовать дёшево (RENAMED
|
||||
Requirements) — не дроби преждевременно в маленьком проекте.
|
||||
|
||||
RFC 2119 — требование валидатора, не стиль:
|
||||
- Каждое ### Requirement ОБЯЗАНО содержать литерал SHALL или MUST, иначе
|
||||
`openspec validate` падает. Поэтому эти слова и WHEN/THEN не русифицируем.
|
||||
|
||||
Что это за проект — читай перед предложением, а не отсюда:
|
||||
- docs/passport.md — цель, её граница (чем проект НЕ является), потребители,
|
||||
типовые сценарии, референсы;
|
||||
- CLAUDE.md — инварианты с severity и семантика гейта;
|
||||
- docs/architecture.md — устройство; docs/security.md — периметр;
|
||||
docs/adr/ — почему решено так; docs/research/ — что уже измерено.
|
||||
Пересказа этих документов здесь нет намеренно: второй дом факта расходится с
|
||||
первым молча, и заметно это становится в предложении, которое уже написано.
|
||||
|
||||
Ревью: правило выбора метки и состав проходов здесь не пересказываем — их дом
|
||||
скилл av-dev-pipeline:review-pipeline, проектная настройка — docs/review.md.
|
||||
|
||||
Конвенции кода: механизированное проверяет гейт, прозой остаётся
|
||||
docs/conventions/. Ни состав шагов гейта, ни перечень конвенций здесь не
|
||||
пересказываем: и то и другое растёт по ходу задач.
|
||||
|
||||
Развилка или блокер — сперва prior art. Готовые решения смотрим в референсах
|
||||
паспорта, отвергаем — с названной причиной, и причина идёт в design.md этого
|
||||
же изменения.
|
||||
|
||||
rules:
|
||||
proposal:
|
||||
- Capabilities называй по поведению или домену системы, не по пакету кода
|
||||
specs:
|
||||
# Кавычки обязательны: без них YAML обрежет строку на первом '#'.
|
||||
- "Каждое ### Requirement обязано содержать SHALL или MUST (иначе валидация падает)"
|
||||
- "Сценарий — ровно #### (четыре решётки); три или список молча теряются"
|
||||
- "SHALL/MUST должно стоять в ПЕРВОМ абзаце требования: валидатор смотрит только его"
|
||||
- "Заголовки и WHEN/THEN/GIVEN — на английском, остальной текст на русском"
|
||||
```
|
||||
|
||||
**Четыре правила для `specs` сняты отказами валидатора, а не выведены из
|
||||
документации** — потому и записаны дословно: без них каждое второе предложение
|
||||
узнаёт их падением `openspec validate --strict`. Блок `context` проект
|
||||
дополняет своим (стек, разведка, особенности домена), но **адреса паспорта и
|
||||
`CLAUDE.md` обязательны** — их отсутствие `docs.py check` называет отказом.
|
||||
|
||||
**Ключи под `rules:` — имена артефактов схемы**, а не свободные слова:
|
||||
`proposal`, `specs`, `design`, `tasks`. Правило под чужим именем не применяется
|
||||
и об этом не сообщает, поэтому `rules.spec` вместо `rules.specs` даёт конфиг,
|
||||
выглядящий написанным и не работающий; `docs.py check` такой ключ называет.
|
||||
Перечень артефактов задаёт OpenSpec, а не канон, — за его актуальностью следит
|
||||
`docs.py openspec-form`.
|
||||
**Форму не проверяет и `docs.py`** — с канона 10 он о файле молчит вовсе.
|
||||
Проверяет её тот же владелец: скилл `av-dev-code:openspec`, команда
|
||||
`openspec.py check`. Плагина конвейера в проекте может не быть — тогда форму не
|
||||
смотрит никто, и это строка доклада, а не поломка. Что канон о файле всё же
|
||||
говорит (единственный дом, а не форма) — [canon.md](canon.md), раздел
|
||||
`openspec/config.yaml`.
|
||||
|
||||
## `docs/.pm.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"canon": 7
|
||||
"canon": <текущая версия>
|
||||
}
|
||||
```
|
||||
|
||||
Плюс `"migrations": "<путь>"`, если есть БД. Ключ `"tasks"` заводится **только**
|
||||
когда имя файла или заголовка отличается от умолчания (`{"backlog":
|
||||
"INDEX.md"}`); секций беклога в нём нет — их дом заголовки `##` индекса. Состав
|
||||
ключей — [canon.md](canon.md).
|
||||
`<текущая версия>` подставляет `init` или `adopt`, целым числом; берётся она из
|
||||
`docs.py version` (строка «канон скрипта»), а не из памяти. Литерал здесь
|
||||
протухает при каждом повышении канона, поэтому его тут и нет: незамещённый
|
||||
плейсхолдер ломает разбор JSON громко, а отставшее число дало бы дрейф молча.
|
||||
|
||||
Плюс `"migrations": "<путь>"`, если есть БД. Ключа `"tasks"` здесь **нет**:
|
||||
настройки каталога задач переехали в свой файл `<каталог задач>/.tasks.json`,
|
||||
потому что ведёт их другой плагин. Состав ключей — [canon.md](canon.md).
|
||||
+17
-292
@@ -25,7 +25,7 @@ from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import NoReturn
|
||||
|
||||
CANON_VERSION = 7
|
||||
CANON_VERSION = 12
|
||||
|
||||
OK, DRIFT, USAGE, ENV, INTERNAL = 0, 1, 2, 3, 4
|
||||
|
||||
@@ -76,42 +76,15 @@ DOC_EXTRA = {
|
||||
"adr": {"template.md": "шаблон записи ADR"},
|
||||
}
|
||||
|
||||
# Настройка OpenSpec. Команда заведения — она же в скилле init; здесь потому,
|
||||
# что её печатает отказ, а отказ без команды заставляет искать её в другом месте.
|
||||
OPENSPEC_INIT = "openspec init --tools claude"
|
||||
|
||||
# Адреса, которые обязан назвать блок context. Не пересказ документов, а именно
|
||||
# ссылки: предложение пишется до того, как кто-либо откроет docs/, и без этих
|
||||
# двух строк его пишут, не зная ни границы домена, ни инвариантов. Список
|
||||
# короткий намеренно — длинный превращает context во второй дом фактов.
|
||||
OPENSPEC_POINTERS = [
|
||||
("passport", "граница домена и «чем НЕ является» останутся непрочитанными"),
|
||||
("CLAUDE.md", "инварианты и семантика гейта останутся непрочитанными"),
|
||||
]
|
||||
|
||||
# --- Форма config.yaml сверена с живым OpenSpec ------------------------------
|
||||
# Служебное в docs/ и каталог задач, оставшийся там от прежней раскладки. Формы
|
||||
# у них скрипт не проверяет, и по разным причинам: `.pm.json` не markdown, а
|
||||
# задачи **принадлежат другому плагину** — `av-dev-tasks`, со своим скриптом,
|
||||
# своим конфигом и своей версией формата.
|
||||
#
|
||||
# Три константы ниже — **слепок чужого инструмента**, а не наше решение. Схема,
|
||||
# перечень артефактов и версия, на которой это проверено, живут в OpenSpec и
|
||||
# меняются без нашего участия; здесь они записаны, чтобы проверка шла без запуска
|
||||
# node на каждом прогоне.
|
||||
#
|
||||
# Слепок стареет, и потому есть кто, кто это замечает: `check` сравнивает
|
||||
# major.minor установленного OpenSpec с OPENSPEC_CHECKED и, если они разошлись,
|
||||
# говорит замечанием «форма не перепроверена». Перепроверяет `docs.py
|
||||
# openspec-form` — он спрашивает сам инструмент и печатает, что разошлось.
|
||||
# Патч-версия сравнением намеренно не берётся: форма конфига в ней не меняется, а
|
||||
# замечание на каждый багфикс приучило бы пролистывать весь блок.
|
||||
OPENSPEC_CHECKED = "1.5"
|
||||
OPENSPEC_SCHEMA = "spec-driven"
|
||||
|
||||
# Артефакты схемы. Ключ `rules:` адресуется артефакту, и адресованный
|
||||
# несуществующему **молча не действует** — ровно тот класс, ради которого вся
|
||||
# проверка и заведена.
|
||||
OPENSPEC_ARTIFACTS = ("proposal", "specs", "design", "tasks")
|
||||
|
||||
# Служебное в docs/ и каталог, который ведёт tasks.py. Оба процессные, но
|
||||
# проверок формы у них нет: .pm.json не markdown, tasks/ ведёт другой скрипт.
|
||||
# Дом задач с версии 11 — `tasks/` в корне репозитория, то есть вне `docs/`
|
||||
# вовсе. `docs/tasks/` здесь терпится потому, что непереехавший проект не должен
|
||||
# получать «файл вне канона» вдобавок к записи журнала, которая и так велит ему
|
||||
# переехать. Внутрь скрипт не смотрит ни в том, ни в другом случае.
|
||||
NOT_DOCS = {".pm.json", "tasks"}
|
||||
|
||||
# Слоты, которых в каноне нет, — с адресом, куда уезжает содержимое. Имена,
|
||||
@@ -120,11 +93,11 @@ NOT_DOCS = {".pm.json", "tasks"}
|
||||
RETIRED = {
|
||||
"review-brief.md": "документы канона и есть бриф; остаток — в review",
|
||||
"review-journal.md": "→ документ review",
|
||||
"plan.md": "→ docs/tasks/ROADMAP.md",
|
||||
"plan.md": "→ tasks/ROADMAP.md (плагин av-dev-tasks)",
|
||||
"local-research.md": "→ документ research",
|
||||
"specs": "поведение → openspec/specs/, обзор → тема architecture",
|
||||
"drafts": "идея → запись research, отказ → ADR, порядок → ROADMAP.md",
|
||||
"backlog": "→ docs/tasks/",
|
||||
"backlog": "→ tasks/ в корне репозитория (плагин av-dev-tasks)",
|
||||
}
|
||||
|
||||
# --- Слаги в именах файлов --------------------------------------------------
|
||||
@@ -241,7 +214,7 @@ def strip_code(text: str) -> str:
|
||||
"""Выкинуть блоки кода и вставки в обратных кавычках.
|
||||
|
||||
Путь в примере или в шаблоне — не ссылка, и краснеть на нём значит краснеть
|
||||
на каждом образце документа. Инлайн-код тоже: `[docs/backlog](docs/tasks/…)`
|
||||
на каждом образце документа. Инлайн-код тоже: `[docs/backlog](tasks/…)`
|
||||
в тексте про подписи ссылок — иллюстрация, а не ссылка."""
|
||||
out, inside = [], False
|
||||
for line in text.splitlines():
|
||||
@@ -489,153 +462,6 @@ def doc_text(root: Path, name: str) -> str | None:
|
||||
)
|
||||
|
||||
|
||||
def rules_keys(live: str) -> list[str]:
|
||||
"""Имена артефактов, которым адресованы правила, — и только они.
|
||||
|
||||
Идём от строки `rules:` до следующего ключа нулевой колонки, а не ищем
|
||||
отступ по всему файлу: блок `context: |` — литеральный скаляр, внутри него
|
||||
строки вида «Language: Russian» и «av-dev-pm:review-pipeline» выглядят
|
||||
ключами и дали бы находку на ровном месте. Проверено на живом конфиге,
|
||||
который так и падал.
|
||||
"""
|
||||
out: list[str] = []
|
||||
inside = False
|
||||
for line in live.splitlines():
|
||||
if not line.strip():
|
||||
continue
|
||||
if not line[0].isspace():
|
||||
inside = line.startswith("rules:")
|
||||
continue
|
||||
if not inside:
|
||||
continue
|
||||
m = re.fullmatch(r" ([A-Za-z_-]+):\s*", line)
|
||||
if m:
|
||||
out.append(m.group(1))
|
||||
return out
|
||||
|
||||
|
||||
def check_openspec(root: Path, rep: Report) -> None:
|
||||
"""Настройка OpenSpec заведена и не осталась примером из коробки.
|
||||
|
||||
Разбираем текстом, а не YAML-парсером: у скриптов канона ноль внешних
|
||||
зависимостей, а PyYAML в стандартной библиотеке нет. Всё, что проверяется
|
||||
ниже, различимо построчно, и ложных срабатываний это не даёт: комментарии
|
||||
отброшены, ключи верхнего уровня стоят в первой колонке.
|
||||
"""
|
||||
os_dir = root / "openspec"
|
||||
if not os_dir.is_dir():
|
||||
rep.error(
|
||||
"нет openspec/ — там дом темы requirements (openspec/specs/) и "
|
||||
f"настройка генерации артефактов; заводится `{OPENSPEC_INIT}`"
|
||||
)
|
||||
return
|
||||
|
||||
if (os_dir / "config.yml").is_file():
|
||||
rep.error(
|
||||
"openspec/config.yml — читается только config.yaml, и этот файл "
|
||||
"останется незамеченным: настройка будет пустой, а выглядеть будет "
|
||||
"заполненной"
|
||||
)
|
||||
|
||||
path = os_dir / "config.yaml"
|
||||
if not path.is_file():
|
||||
rep.error(
|
||||
"нет openspec/config.yaml — язык, правила именования capability и "
|
||||
"придирки валидатора будут заново угадываться на каждом предложении"
|
||||
)
|
||||
return
|
||||
|
||||
text = path.read_text(encoding="utf-8")
|
||||
live = "\n".join(
|
||||
line for line in text.splitlines() if not line.lstrip().startswith("#")
|
||||
)
|
||||
keys = set(re.findall(r"(?m)^([A-Za-z_]+):", live))
|
||||
|
||||
schema = re.search(r"(?m)^schema:\s*(\S+)", live)
|
||||
if schema is None:
|
||||
rep.error(
|
||||
f"в openspec/config.yaml нет ключа schema — ожидается {OPENSPEC_SCHEMA}"
|
||||
)
|
||||
elif schema.group(1) != OPENSPEC_SCHEMA:
|
||||
rep.error(
|
||||
f"schema в openspec/config.yaml — {schema.group(1)}, а канон описан "
|
||||
f"для {OPENSPEC_SCHEMA}"
|
||||
)
|
||||
|
||||
if "context" not in keys:
|
||||
rep.error(
|
||||
"в openspec/config.yaml нет ключа context: файл остался примером из "
|
||||
"коробки — предложение пишется без языка, правил именования "
|
||||
"capability и адресов документов проекта"
|
||||
)
|
||||
else:
|
||||
for pointer, why in OPENSPEC_POINTERS:
|
||||
if pointer not in live:
|
||||
rep.error(
|
||||
f"openspec/config.yaml не называет {pointer} — {why}"
|
||||
)
|
||||
|
||||
if "rules" not in keys or "specs:" not in live:
|
||||
rep.error(
|
||||
"в openspec/config.yaml нет rules.specs — придирки валидатора "
|
||||
"нигде не записаны, и каждое предложение узнаёт их отказом"
|
||||
)
|
||||
elif "SHALL" not in live:
|
||||
rep.error(
|
||||
"rules.specs в openspec/config.yaml не называет SHALL — "
|
||||
"требование без этого литерала валидатор отвергает, а правило "
|
||||
"проекта об этом молчит"
|
||||
)
|
||||
|
||||
# Ключ под rules: — имя артефакта схемы. Опечатка или устаревшее имя не
|
||||
# ломает ничего видимого: правила просто не применяются, а конфиг выглядит
|
||||
# написанным.
|
||||
for name in rules_keys(live):
|
||||
if name not in OPENSPEC_ARTIFACTS:
|
||||
rep.error(
|
||||
f"rules.{name} в openspec/config.yaml — такого артефакта у схемы "
|
||||
f"{OPENSPEC_SCHEMA} нет ({', '.join(OPENSPEC_ARTIFACTS)}): правила "
|
||||
f"под ним не применяются и молчат об этом"
|
||||
)
|
||||
|
||||
check_openspec_fresh(rep)
|
||||
|
||||
|
||||
def openspec_cli(args: list[str]) -> str | None:
|
||||
"""Спросить сам инструмент. None — его нет или он не ответил."""
|
||||
try:
|
||||
out = subprocess.run(
|
||||
["openspec", *args], capture_output=True, text=True, timeout=30
|
||||
)
|
||||
except (FileNotFoundError, OSError, subprocess.SubprocessError):
|
||||
return None
|
||||
return out.stdout.strip() if out.returncode == 0 else None
|
||||
|
||||
|
||||
def check_openspec_fresh(rep: Report) -> None:
|
||||
"""Не устарел ли наш слепок формы config.yaml.
|
||||
|
||||
Стоит один запуск `openspec --version` — десятые доли секунды. Перечень
|
||||
артефактов и имя схемы отсюда не спрашиваются намеренно: они стоят втрое
|
||||
дороже, а меняются только вместе с версией, и потому за ними ходит отдельная
|
||||
команда `openspec-form`, а эта проверка говорит, когда её звать.
|
||||
"""
|
||||
got = openspec_cli(["--version"])
|
||||
if got is None:
|
||||
rep.skip(
|
||||
"openspec не отвечает (нет на PATH?) — актуальность формы "
|
||||
"config.yaml не проверялась"
|
||||
)
|
||||
return
|
||||
installed = ".".join(got.split(".")[:2])
|
||||
if installed != OPENSPEC_CHECKED:
|
||||
rep.note(
|
||||
f"форма openspec/config.yaml сверена с OpenSpec {OPENSPEC_CHECKED}, "
|
||||
f"установлен {got}: перепроверить — `docs.py openspec-form`. Пока не "
|
||||
f"перепроверено, проверки формы судят по прежней схеме"
|
||||
)
|
||||
|
||||
|
||||
def check_capabilities(root: Path, rep: Report) -> None:
|
||||
specs = root / "openspec" / "specs"
|
||||
text = doc_text(root, "architecture")
|
||||
@@ -722,35 +548,6 @@ def check_migrations(root: Path, cfg: dict, base: str | None, rep: Report) -> No
|
||||
)
|
||||
|
||||
|
||||
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
|
||||
# cwd=root обязателен: tasks.py отвергает --dir вне текущего каталога, и без
|
||||
# этого его отказ окружения (код 3) схлопнулся бы в наш дрейф (код 1).
|
||||
proc = subprocess.run(
|
||||
[sys.executable, str(script), "check", "--dir", "docs/tasks"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
cwd=str(root),
|
||||
)
|
||||
if proc.returncode == 0:
|
||||
return
|
||||
if proc.returncode == 1:
|
||||
rep.error("tasks.py check нашёл дрейф в docs/tasks/ — разбирать его командой tasks.py")
|
||||
else:
|
||||
# Чужой код выхода не выдаём за свой: 3 это окружение, а не дрейф.
|
||||
rep.skip(
|
||||
f"tasks.py check не отработал (код {proc.returncode}): "
|
||||
f"{(proc.stderr or proc.stdout).strip().splitlines()[0] if (proc.stderr or proc.stdout).strip() else 'без сообщения'}"
|
||||
)
|
||||
|
||||
|
||||
# --- Отчёт ------------------------------------------------------------------
|
||||
|
||||
|
||||
@@ -769,10 +566,11 @@ def report(rep: Report) -> int:
|
||||
print(f" {msg}")
|
||||
|
||||
print(
|
||||
"\nМашина проверила раскладку, имена файлов, ссылки, версию, форму\n"
|
||||
"openspec/config.yaml и две сверки с кодом. Согласованность документов\n"
|
||||
"между собой и с кодом она не проверяет — как и то, ссылается ли\n"
|
||||
"config.yaml на документы или пересказывает их. Это суждение агентов\n"
|
||||
"\nМашина проверила раскладку, имена файлов, ссылки, версию и две\n"
|
||||
"сверки с кодом. Форму openspec/config.yaml она не проверяет: каталог\n"
|
||||
"принадлежит конвейеру, и форму смотрит его скрипт\n"
|
||||
"(`av-dev-code:openspec`, команда `openspec.py check`). Согласованность\n"
|
||||
"документов между собой и с кодом — тоже не её: это суждение агентов\n"
|
||||
"`doc-consistency` (документ ↔ документ ↔ openspec) и `doc-code-drift`\n"
|
||||
"(документ ↔ код)."
|
||||
)
|
||||
@@ -798,10 +596,8 @@ def cmd_check(args: argparse.Namespace) -> int:
|
||||
check_slugs(root, rep)
|
||||
check_links(root, rep)
|
||||
check_placeholders_and_debt(root, rep)
|
||||
check_openspec(root, rep)
|
||||
check_capabilities(root, rep)
|
||||
check_migrations(root, cfg, args.base, rep)
|
||||
check_tasks(root, rep)
|
||||
return report(rep)
|
||||
|
||||
|
||||
@@ -814,71 +610,6 @@ def cmd_version(args: argparse.Namespace) -> int:
|
||||
return OK
|
||||
|
||||
|
||||
def cmd_openspec_form(args: argparse.Namespace) -> int:
|
||||
"""Перепроверить слепок формы config.yaml по живому OpenSpec.
|
||||
|
||||
Ничего не правит и не трогает проект: спрашивает инструмент и печатает, что
|
||||
разошлось с константами скрипта. Чинит человек — правкой констант, скелета в
|
||||
skeletons.md и записью в журнал версий канона, если форма действительно
|
||||
поменялась.
|
||||
"""
|
||||
version = openspec_cli(["--version"])
|
||||
if version is None:
|
||||
fail(
|
||||
ENV,
|
||||
"openspec не отвечает: поставь его или проверь PATH — "
|
||||
"перепроверять форму нечем",
|
||||
)
|
||||
raw = openspec_cli(["templates", "--json"])
|
||||
if raw is None:
|
||||
fail(ENV, "`openspec templates --json` не отработал — схему не спросить")
|
||||
try:
|
||||
artifacts = tuple(json.loads(raw))
|
||||
except json.JSONDecodeError as exc:
|
||||
fail(ENV, f"`openspec templates --json` отдал неразбираемое: {exc}")
|
||||
|
||||
print(f"OpenSpec установлен: {version}")
|
||||
print(f"форма сверена с: {OPENSPEC_CHECKED}")
|
||||
print(f"артефакты схемы: {', '.join(artifacts)}")
|
||||
print(f"записано в скрипте: {', '.join(OPENSPEC_ARTIFACTS)}")
|
||||
|
||||
diffs: list[str] = []
|
||||
if ".".join(version.split(".")[:2]) != OPENSPEC_CHECKED:
|
||||
diffs.append(
|
||||
f"версия: поднять OPENSPEC_CHECKED до "
|
||||
f"{'.'.join(version.split('.')[:2])} — но только после того, как "
|
||||
f"остальные строки этого отчёта сойдутся"
|
||||
)
|
||||
for name in artifacts:
|
||||
if name not in OPENSPEC_ARTIFACTS:
|
||||
diffs.append(
|
||||
f"новый артефакт {name}: решить, нужны ли ему правила в rules, "
|
||||
f"и добавить имя в OPENSPEC_ARTIFACTS"
|
||||
)
|
||||
for name in OPENSPEC_ARTIFACTS:
|
||||
if name not in artifacts:
|
||||
diffs.append(
|
||||
f"артефакта {name} у схемы больше нет: правила под ним в конфигах "
|
||||
f"проектов молчат — убрать из OPENSPEC_ARTIFACTS, из скелета и "
|
||||
f"записать в журнал версий канона"
|
||||
)
|
||||
|
||||
print()
|
||||
if not diffs:
|
||||
print("Слепок сходится. Осталось глазами: не изменились ли придирки")
|
||||
print("валидатора — их скрипт проверить не может, они проявляются только")
|
||||
print("отказом `openspec validate --strict` на живой спеке.")
|
||||
return OK
|
||||
print("Разошлось:")
|
||||
for line in diffs:
|
||||
print(f" - {line}")
|
||||
print()
|
||||
print("Правится в трёх местах сразу: константы этого скрипта, скелет")
|
||||
print("`openspec/config.yaml` в skeletons.md и запись в changelog.md —")
|
||||
print("иначе проекты останутся на прежней форме молча.")
|
||||
return DRIFT
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="docs.py",
|
||||
@@ -895,12 +626,6 @@ def main() -> int:
|
||||
p_ver.add_argument("--dir", default=".", help="корень проекта")
|
||||
p_ver.set_defaults(func=cmd_version)
|
||||
|
||||
p_form = sub.add_parser(
|
||||
"openspec-form",
|
||||
help="перепроверить форму config.yaml по живому OpenSpec",
|
||||
)
|
||||
p_form.set_defaults(func=cmd_openspec_form)
|
||||
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
return args.func(args)
|
||||
@@ -9,8 +9,8 @@ description: Вести содержимое документов канона
|
||||
Определение канона и роли документов — [канон](../canon/references/canon.md),
|
||||
здесь не пересказывается.
|
||||
|
||||
Главный вызывающий — **шаг синка документации в пайплайне задачи**. Пайплайн
|
||||
живёт в другом плагине и зовёт этот скилл по имени; проект без пайплайна ведёт
|
||||
Главный вызывающий — **шаг синка документации в конвейере задачи**. Конвейер
|
||||
живёт в другом плагине и зовёт этот скилл по имени; проект без конвейера ведёт
|
||||
документацию тем же скиллом вручную.
|
||||
|
||||
## Правило, из которого всё следует
|
||||
@@ -55,25 +55,39 @@ description: Вести содержимое документов канона
|
||||
- passport, security, conventions, review — не требуется: изменение внутреннее
|
||||
```
|
||||
|
||||
## Сверка — не здесь, а на сессии
|
||||
## Сверка — не здесь, а в `av-dev-docs:healthcheck`
|
||||
|
||||
Синк правит документы поодиночке, а расходятся они **между собой**: факт,
|
||||
дописанный в `architecture.md`, уже живёт в `CLAUDE.md`; периметр в
|
||||
`security.md` не знает про новый эндпоинт. Поймать это на своей же правке нельзя,
|
||||
и судит это агент `doc-consistency`.
|
||||
|
||||
**Но синк его не зовёт.** Оба судьи документов — `doc-consistency` и
|
||||
`doc-code-drift` — зовутся раз в спринт, шагом сессии, на весь канон разом.
|
||||
Причина в цене: `doc-consistency` на `opus` по каждой сделанной задаче — самая
|
||||
дорогая церемония процесса, а `doc-code-drift` хоть и на `sonnet`, но читает
|
||||
репозиторий целиком. К тому же расхождение между двумя документами по определению
|
||||
требует двух документов, а на большинстве задач синк правит один.
|
||||
**Но синк его не зовёт.** Обоими судьями документов владеет скилл
|
||||
`av-dev-docs:healthcheck`, и зовут их на весь канон разом, а не на пачку,
|
||||
отобранную работой. Причина в цене: `doc-consistency` на `opus` по каждой
|
||||
сделанной задаче — самая дорогая церемония процесса, а `doc-code-drift` хоть и на
|
||||
`sonnet`, но читает репозиторий целиком. К тому же расхождение между двумя
|
||||
документами по определению требует двух документов, а на большинстве задач синк
|
||||
правит один.
|
||||
|
||||
Что теряется: привязка находки к задаче, которая её породила. Что выигрывается,
|
||||
кроме денег: синк перестаёт отбирать пачку, и в неё попадают документы,
|
||||
которых работа не касалась, — расхождение, внесённое правкой в одном месте, там
|
||||
и живёт.
|
||||
|
||||
## Вычитка — наоборот, здесь
|
||||
|
||||
**Язык правленого вычитывается на синке, и зовёшь агента `doc-wording` ты.**
|
||||
Довод обратный доводу про судей: он читает **только названную пачку**, стоит
|
||||
дёшево и ищет ровно то, что портится в момент письма, — залог, оценку без факта,
|
||||
жаргон, термин без ввода. Ждать `healthcheck` здесь нечего: через месяц никто уже
|
||||
не помнит, какую фразу имел в виду автор.
|
||||
|
||||
Позови его **последним шагом синка**, отдав список файлов, которых чек-лист
|
||||
коснулся, — и назови этот список в промпте: по нему же он судит, известен ли
|
||||
термин. Ничего не правивший синк агента не зовёт. Находки он отдаёт готовыми
|
||||
формулировками, подставляешь их ты.
|
||||
|
||||
## ADR — промоут, а не второе сочинение
|
||||
|
||||
Обоснование уже написано: `opsx:propose` кладёт `design.md` в каждый change, и
|
||||
@@ -116,13 +130,50 @@ description: Вести содержимое документов канона
|
||||
дать этому остаться в контексте. Признак: ты правил разбор, опираясь на то, чего
|
||||
нет ни в одном документе.
|
||||
|
||||
## Обращение к соседним плагинам
|
||||
|
||||
Два раздела ниже — запись в `review.md` и промоут в конвенции — берут форму у
|
||||
конвейера ревью: она принадлежит ему, а не канону. Берут **вызовом скилла**, а не
|
||||
чтением файла по пути.
|
||||
|
||||
**Копия.** Дом правила — `shared/plugin-boundary.md` в репозитории плагинов.
|
||||
Правится дом, а не этот файл.
|
||||
|
||||
<!-- копия: граница-плагинов из shared/plugin-boundary.md -->
|
||||
|
||||
Плагины `av-dev` ставятся порознь, и ни один не вправе считать, что сосед на
|
||||
месте.
|
||||
|
||||
**Чужой скилл зовётся полным именем** — `av-dev-docs:canon`, `av-dev-tasks:tasks`,
|
||||
`av-dev-code:review`. Короткое имя может разрешиться в устаревшую проектную копию
|
||||
из `.claude/skills/`, и подмены не будет видно ни в докладе, ни в поведении.
|
||||
|
||||
**Путь в дерево чужого плагина не пишется никогда.** `$CLAUDE_PLUGIN_ROOT` ведёт
|
||||
только в свой плагин; вычисленный от него путь к соседу либо не откроется, либо
|
||||
откроет чужую установку. Нужен чужой справочник — зови владеющий им скилл, он
|
||||
прочитает его сам.
|
||||
|
||||
**Вызов не разрешился — плагина в проекте нет.** Это исход, а не поломка: назови
|
||||
строкой доклада, чего теперь не делает никто, и продолжай работу. Молчать нельзя,
|
||||
пропуск неотличим от сделанного; выдумывать обходной путь нельзя тоже.
|
||||
|
||||
**Присутствие узнаётся вызовом или следом в проекте, но не объявлением.** Перечня
|
||||
установленных плагинов проект не ведёт — он разошёлся бы с действительностью
|
||||
молча. Что сосед здесь работал, видно по заведённому им файлу: `docs/.pm.json` —
|
||||
канон, `<каталог задач>/.tasks.json` — задачи, `openspec/config.yaml` — конвейер.
|
||||
|
||||
<!-- /копия: граница-плагинов -->
|
||||
|
||||
Чем оборачивается отсутствие конвейера — в каждом из двух разделов отдельно: без
|
||||
него работа не отменяется, отменяется только его процедура.
|
||||
|
||||
## Запись в `review.md`
|
||||
|
||||
Файл держит два раздела с разными сроками жизни — журнал дефектов и настройку
|
||||
конвейера. **Что в каком и в какой форме — в
|
||||
[каноне](../canon/references/canon.md), раздел `review.md`**; подробности формы
|
||||
записи и выбор адреса, куда она ведёт, — у конвейера ревью проекта (при
|
||||
`av-dev-pipeline` — `Skill av-dev-pipeline:review-pipeline`, его
|
||||
`av-dev-code` — `Skill av-dev-code:review`, его
|
||||
`references/review-journal.md`). **Конвейера в проекте нет** — пиши по форме из
|
||||
скелета `review.md`, которую положил канон, и скажи в докладе, что подробностей
|
||||
формы взять негде.
|
||||
@@ -130,13 +181,13 @@ description: Вести содержимое документов канона
|
||||
Твоя часть на синке: **дефект пишется сразу**, а не «потом, когда починим».
|
||||
Со временем теряется не факт, а то, почему дефект не поймали, — единственное,
|
||||
ради чего журнал есть. И решение сузить проверки (перестали звать проход, понизили
|
||||
метка) обязано попасть в раздел настройки, а не остаться в отчёте ревью.
|
||||
метку) обязано попасть в раздел настройки, а не остаться в отчёте ревью.
|
||||
|
||||
## Промоут в конвенции
|
||||
|
||||
Находка → конвенция → правило линтера → **удаление из прозы**. Процедура целиком
|
||||
принадлежит конвейеру ревью проекта (при `av-dev-pipeline` — его
|
||||
`references/promote.md`, читается через `Skill av-dev-pipeline:review-pipeline`);
|
||||
принадлежит конвейеру ревью проекта (при `av-dev-code` — его
|
||||
`references/promote.md`, читается через `Skill av-dev-code:review`);
|
||||
роль каталога конвенций — в [каноне](../canon/references/canon.md). **Конвейера
|
||||
нет** — три шага всё равно твои, просто без его процедуры: сформулируй правило,
|
||||
поищи, чем оно механизируется, и вычеркни прозу, если механизировалось.
|
||||
@@ -0,0 +1,141 @@
|
||||
---
|
||||
name: healthcheck
|
||||
description: "Проверка здоровья документации проекта судом, а не машиной: не разошлись ли документы между собой и с кодом. Зовёт двух агентов на весь канон разом — doc-consistency (один факт в двух домах, прямое противоречие, поведение в architecture.md вместо спек, ADR без парного статуса, число без провенанса) и doc-code-drift (протухший факт: имя ветки, команды, пути, зависимости поимённо, настройки с числом, единые точки, capability). Разбирает урожай порциями: строка на замену идёт в документ сразу, работа больше абзаца становится задачей. Использовать, когда с прошлой сверки сделан десяток задач, когда вернулись к проекту после перерыва, перед тем как опереться на документ в решении, а также шагом adopt и upgrade. Дорого — не на каждой задаче. Раскладку и версию канона проверяет скилл canon, язык документов — агент doc-wording."
|
||||
---
|
||||
|
||||
# Здоровье документации
|
||||
|
||||
Проверяет то, **чего машина не видит**: разошлись ли документы между собой и с
|
||||
кодом. Раскладка, версия, битые ссылки, нетронутые плейсхолдеры — это `canon
|
||||
check` и его скрипт; здесь начинается там, где кончается `docs.py`.
|
||||
|
||||
Разрез проверяемый: **машина сверяет форму, этот скилл — утверждения**. «В
|
||||
`architecture.md` есть раздел» проверит скрипт. «В `architecture.md` написано,
|
||||
что зависимость одна, а в манифесте их три» — суждение, и его выносит агент.
|
||||
|
||||
## Когда звать
|
||||
|
||||
**Зовёт человек**, но признак наблюдаемый, а не календарный:
|
||||
|
||||
- **с прошлой сверки сделан десяток задач.** Документы протухают ровно от
|
||||
сделанной работы: переименованная цель сборки, ушедшая зависимость, второй
|
||||
способ делать то, что обзор объявил единственным, факт, дописанный в
|
||||
`architecture.md` и уже живущий в `CLAUDE.md`;
|
||||
- **вернулись к проекту после перерыва** — прежде чем опираться на написанное;
|
||||
- **перед тем как опереться на документ в решении**, если оно дорогое;
|
||||
- шагом `adopt` и шагом `upgrade` — их зовёт скилл `canon` сам.
|
||||
|
||||
**Не на каждой задаче и не на каждом синке документации.** Цена реальная:
|
||||
`doc-consistency` идёт на `opus`, потому что сличение утверждений — суждение;
|
||||
`doc-code-drift` хоть и на `sonnet`, но читает репозиторий целиком. Прогон по
|
||||
каждой сделанной задаче был бы самой дорогой церемонией процесса, а находок дал
|
||||
бы почти те же: документы расходятся не с одной задачи, а с десятка.
|
||||
|
||||
Прежде оба звались шагом сессии между спринтами. Спринтов нет, и **момент
|
||||
пришлось назвать заново** — иначе их не звал бы никто, кроме разовых `adopt` и
|
||||
`upgrade`, то есть на живом проекте никогда.
|
||||
|
||||
## Обращение к соседним плагинам
|
||||
|
||||
**Копия.** Дом — `shared/plugin-boundary.md` в репозитории плагинов. Правится
|
||||
дом, а не этот файл.
|
||||
|
||||
<!-- копия: граница-плагинов из shared/plugin-boundary.md -->
|
||||
|
||||
Плагины `av-dev` ставятся порознь, и ни один не вправе считать, что сосед на
|
||||
месте.
|
||||
|
||||
**Чужой скилл зовётся полным именем** — `av-dev-docs:canon`, `av-dev-tasks:tasks`,
|
||||
`av-dev-code:review`. Короткое имя может разрешиться в устаревшую проектную копию
|
||||
из `.claude/skills/`, и подмены не будет видно ни в докладе, ни в поведении.
|
||||
|
||||
**Путь в дерево чужого плагина не пишется никогда.** `$CLAUDE_PLUGIN_ROOT` ведёт
|
||||
только в свой плагин; вычисленный от него путь к соседу либо не откроется, либо
|
||||
откроет чужую установку. Нужен чужой справочник — зови владеющий им скилл, он
|
||||
прочитает его сам.
|
||||
|
||||
**Вызов не разрешился — плагина в проекте нет.** Это исход, а не поломка: назови
|
||||
строкой доклада, чего теперь не делает никто, и продолжай работу. Молчать нельзя,
|
||||
пропуск неотличим от сделанного; выдумывать обходной путь нельзя тоже.
|
||||
|
||||
**Присутствие узнаётся вызовом или следом в проекте, но не объявлением.** Перечня
|
||||
установленных плагинов проект не ведёт — он разошёлся бы с действительностью
|
||||
молча. Что сосед здесь работал, видно по заведённому им файлу: `docs/.pm.json` —
|
||||
канон, `<каталог задач>/.tasks.json` — задачи, `openspec/config.yaml` — конвейер.
|
||||
|
||||
<!-- /копия: граница-плагинов -->
|
||||
|
||||
Здесь сосед один: `av-dev-tasks:tasks`, когда находка тянет на задачу. Его нет —
|
||||
находки остаются списком в докладе, и это говорится строкой.
|
||||
|
||||
## Пачка — весь канон, и это не расточительство
|
||||
|
||||
Оба агента зовутся **на весь канон разом**, а не на пачку, отобранную работой.
|
||||
|
||||
Когда пачку отбирала работа, без присмотра оставалось ровно то, чего работа не
|
||||
касалась: правка, отменившая решение, живёт в одном документе, а парный статус
|
||||
нужен в другом; факт, продублированный год назад, не попадёт ни в один диапазон
|
||||
диффа. Канон мал — он читается целиком, и цена этого известна заранее.
|
||||
|
||||
## Кого зовёшь и что передаёшь
|
||||
|
||||
| Агент | Что смотрит | Читает | Модель |
|
||||
| --- | --- | --- | --- |
|
||||
| `doc-consistency` | смысловой дубль, прямое противоречие между документами, поведение в `architecture.md` вместо спек, ADR без ссылки и парного статуса, число без провенанса, заглушка вместо честной строки | `docs/`, `openspec/` | `opus` |
|
||||
| `doc-code-drift` | протухший факт: имя ветки, команды, пути, зависимости поимённо, настройки с числом, единые точки проекта, capability | весь репозиторий | `sonnet` |
|
||||
|
||||
**`doc-code-drift` обязан получить раздел запретов `CLAUDE.md`.** Он гоняет
|
||||
команды — только читающие, — и без перечня запретов не знает, чего в этом
|
||||
проекте запускать нельзя. Не передал — он либо остановится, либо тронет то, чего
|
||||
трогать не следовало.
|
||||
|
||||
**Судит не тот, кто писал.** Ни один из двоих ничего не правит: оба возвращают
|
||||
готовые формулировки, подставляешь ты. Самопроверка документа слабее всего ровно
|
||||
там, где формулировка казалась удачной при написании.
|
||||
|
||||
Одного из двух можно позвать отдельно — но **скажи в докладе, кого именно
|
||||
позвал**. Доклад, умолчавший об этом, читается как «сверено целиком».
|
||||
|
||||
## Разбор урожая
|
||||
|
||||
Находки — обычный материал правки, и разбирать их надо **порциями**, а не одним
|
||||
заходом: тридцать находок подряд получают «принято» не потому, что верны, а
|
||||
потому, что разбор затянулся.
|
||||
|
||||
По каждой находке ровно три исхода:
|
||||
|
||||
1. **Строка на замену** — правь документ сразу. Формулировка уже готова, спорить
|
||||
не с чем, и откладывание превращает её в задачу дороже самой правки.
|
||||
2. **Работа больше чем на абзац** — задача типа `chore`. Заводит её **не этот
|
||||
скилл**: вызови Skill `av-dev-tasks:tasks`, у него свой формат, дедупликация
|
||||
против беклога и кладбища. Плагина нет — отдай списком в докладе и скажи это
|
||||
строкой.
|
||||
3. **Не находка** — агент ошибся, документ прав. Скажи это прямо: неразобранная
|
||||
находка и отклонённая различаются, и вторая экономит время на следующем
|
||||
прогоне. Класс ошибок, который повторяется, идёт в `docs/review.*`, раздел
|
||||
настройки, — там дом типовых ложноположительных.
|
||||
|
||||
## Доклад
|
||||
|
||||
- **Кого позвал** — обоих или одного, и почему одного.
|
||||
- Находки по каждому агенту: сколько, что поправлено сразу, что стало задачей
|
||||
(со слагами), что отклонено и почему.
|
||||
- **Границы покрытия**: что смотрели и чего не смотрели. У `doc-code-drift` она
|
||||
идёт из его собственного отчёта — перечень фактов у него закрытый, и он
|
||||
называет, какие из них проверить было нечем.
|
||||
- Канона в проекте нет вовсе — это исход, а не пустой прогон: скажи строкой и
|
||||
предложи `av-dev-docs:canon`.
|
||||
|
||||
## Чего этот скилл не делает
|
||||
|
||||
- **Не проверяет раскладку, версию и ссылки** — это `canon check`, там машина.
|
||||
- **Не судит язык** документов: залог, англицизмы, жаргон, термин без дома — это
|
||||
агент `doc-wording`, и зовут его отдельно, по пачке правленных документов.
|
||||
Звонящие у него названные — последний шаг синка в `av-dev-docs:docs`, шаг 9
|
||||
`av-dev-docs:init` и шаг вычитки в обоих режимах `canon`, — просто ни один из
|
||||
них не здесь. У него другой ритм: он нужен там, где текст только что писали, а
|
||||
не там, где он год лежал. Оркестровать его нечем — он один и работает по
|
||||
названному списку.
|
||||
- **Не правит документы за агентов** — они возвращают формулировки, решение
|
||||
подставить принимает человек или ты по его правилу.
|
||||
- **Не заводит задачи** — этим владеет `av-dev-tasks:tasks`.
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: init
|
||||
description: "Завести новый проект — сессия вопросов и ответов по свободному описанию замысла, из которой рождается первичная документация по канону av-dev: паспорт, CLAUDE.md с инвариантами и командами, модель угроз с периметром, первые цели в роадмапе и скелет остальных документов. Заводит и OpenSpec (openspec init) с настроенным openspec/config.yaml — дом темы requirements, без которого не работают ни propose, ни ревью. Использовать, когда начинают новый проект с нуля, когда есть только текст «что мне нужно и почему» и надо превратить его в рабочую документацию, когда просят провести стартовое интервью по брифу. Проект, где документация уже как-то ведётся, переводит скилл canon."
|
||||
description: "Завести новый проект — сессия вопросов и ответов по свободному описанию замысла, из которой рождается первичная документация по канону av-dev: паспорт, CLAUDE.md с инвариантами и командами, модель угроз с периметром и скелет остальных документов; первые цели собирает интервью, а записывает их вызовом скилла av-dev-tasks:tasks — роадмап принадлежит плагину задач. OpenSpec заводит не сам, а вызовом скилла av-dev-code:openspec — каталог принадлежит конвейеру; плагина конвейера нет — шаг пропускается строкой доклада. Использовать, когда начинают новый проект с нуля, когда есть только текст «что мне нужно и почему» и надо превратить его в рабочую документацию, когда просят провести стартовое интервью по брифу. Проект, где документация уже как-то ведётся, переводит скилл canon."
|
||||
---
|
||||
|
||||
# Заведение нового проекта
|
||||
@@ -26,13 +26,17 @@ description: "Завести новый проект — сессия вопро
|
||||
| `passport.md` | `architecture.md` |
|
||||
| `CLAUDE.md` | `database.md` |
|
||||
| `security.md` | `conventions/` |
|
||||
| `docs/tasks/ROADMAP.md` — первые цели | `research/`, `adr/` |
|
||||
| `docs/.pm.json` | `review.md` — журнал пуст, настройка появится с первым ревью |
|
||||
| `openspec/config.yaml` | |
|
||||
| `docs/.pm.json` | `research/`, `adr/` |
|
||||
| | `review.md` — журнал пуст, настройка появится с первым ревью |
|
||||
|
||||
Честная строка информативна, а не «TBD»: «архитектуры пока нет: кода нет,
|
||||
заводится первой задачей». Проход читает её как факт.
|
||||
|
||||
**`tasks/ROADMAP.md` в таблице нет намеренно.** Первые цели `init` собирает
|
||||
интервью (блок 6), но записывает их не он: каталогом задач и формой целей владеет
|
||||
`av-dev-tasks:tasks`, и это шаг 7. Плагина нет — цели остаются списком в докладе,
|
||||
роадмапа в проекте не появляется, и это говорится строкой.
|
||||
|
||||
## Порядок интервью — зависимость, а не удобство
|
||||
|
||||
Каждый блок опирается на ответ предыдущего; переставлять нельзя.
|
||||
@@ -65,32 +69,71 @@ description: "Завести новый проект — сессия вопро
|
||||
- **Развилка замысла — человеку, механика — сама.** Имена файлов, слаги, порядок
|
||||
строк не выноси.
|
||||
|
||||
## Обращение к соседним плагинам
|
||||
|
||||
Два шага порядка работы — вызовы чужого: OpenSpec заводит конвейер, каталог задач
|
||||
ведёт плагин задач. Ни того, ни другого `init` не делает руками.
|
||||
|
||||
**Копия.** Дом правила — `shared/plugin-boundary.md` в репозитории плагинов.
|
||||
Правится дом, а не этот файл.
|
||||
|
||||
<!-- копия: граница-плагинов из shared/plugin-boundary.md -->
|
||||
|
||||
Плагины `av-dev` ставятся порознь, и ни один не вправе считать, что сосед на
|
||||
месте.
|
||||
|
||||
**Чужой скилл зовётся полным именем** — `av-dev-docs:canon`, `av-dev-tasks:tasks`,
|
||||
`av-dev-code:review`. Короткое имя может разрешиться в устаревшую проектную копию
|
||||
из `.claude/skills/`, и подмены не будет видно ни в докладе, ни в поведении.
|
||||
|
||||
**Путь в дерево чужого плагина не пишется никогда.** `$CLAUDE_PLUGIN_ROOT` ведёт
|
||||
только в свой плагин; вычисленный от него путь к соседу либо не откроется, либо
|
||||
откроет чужую установку. Нужен чужой справочник — зови владеющий им скилл, он
|
||||
прочитает его сам.
|
||||
|
||||
**Вызов не разрешился — плагина в проекте нет.** Это исход, а не поломка: назови
|
||||
строкой доклада, чего теперь не делает никто, и продолжай работу. Молчать нельзя,
|
||||
пропуск неотличим от сделанного; выдумывать обходной путь нельзя тоже.
|
||||
|
||||
**Присутствие узнаётся вызовом или следом в проекте, но не объявлением.** Перечня
|
||||
установленных плагинов проект не ведёт — он разошёлся бы с действительностью
|
||||
молча. Что сосед здесь работал, видно по заведённому им файлу: `docs/.pm.json` —
|
||||
канон, `<каталог задач>/.tasks.json` — задачи, `openspec/config.yaml` — конвейер.
|
||||
|
||||
<!-- /копия: граница-плагинов -->
|
||||
|
||||
Чем оборачивается отсутствие каждого — на самих шагах 3 и 7. Заведение проекта
|
||||
из-за этого не останавливается: проект без конвейера и без учёта задач законен.
|
||||
|
||||
## Порядок работы
|
||||
|
||||
1. Прочитай бриф целиком. Выпиши, на какие блоки интервью ответ уже есть.
|
||||
2. Проведи интервью итерациями по ≤3 вопроса.
|
||||
3. **Заведи OpenSpec: `openspec init --tools claude`.** Каталог `openspec/` —
|
||||
часть канона, а не соседняя технология: в нём дом темы `requirements`, и без
|
||||
него не работают ни `opsx:propose`, ни ревью дизайна, ни сверка требований.
|
||||
Команда кладёт ещё `.claude/skills/openspec-*` и `.claude/commands/opsx/*` —
|
||||
это её нормальная работа, не трогай их.
|
||||
3. **OpenSpec — вызови Skill `av-dev-code:openspec`.** Он заводит каталог и
|
||||
заменяет пример в `config.yaml` настройкой. Делается это **до первого
|
||||
документа**: без `openspec/` не работают ни `opsx:propose`, ни ревью дизайна,
|
||||
ни сверка требований. Каталог принадлежит конвейеру, а не канону, поэтому
|
||||
здесь только вызов — ни команды, ни формы файла `init` не знает.
|
||||
|
||||
**Вызов не разрешился** — проект без конвейера живёт без OpenSpec законно:
|
||||
строка доклада, и дальше; `docs.py check` о каталоге тоже промолчит.
|
||||
4. Заведи `docs/.pm.json` с текущей версией канона.
|
||||
5. Напиши заполняемые документы. **Бриф переезжает в `passport.md`** и
|
||||
отдельным файлом не остаётся: два дома для одного замысла разойдутся на
|
||||
первом же уточнении.
|
||||
6. Заведи скелет остальных по [скелетам](../canon/references/skeletons.md) —
|
||||
каждый с честной строкой.
|
||||
7. **Заполни `openspec/config.yaml`** по тем же скелетам. Файл из коробки —
|
||||
закомментированный пример на английском; он **заменяется целиком**, потому что
|
||||
нетронутый выглядит настроенным, а работает как пустой. Пиши туда только то,
|
||||
что нужно **в момент порождения артефакта**: язык, правила именования
|
||||
capability, придирки валидатора и **адреса** `docs/passport.md` и `CLAUDE.md`.
|
||||
Инварианты, конвенции и правило ревью не пересказывай — у них есть дома, и
|
||||
второй дом разойдётся с первым молча.
|
||||
8. Каталог задач и первые цели — **вызови скилл `av-dev-pm:tasks`**: он владеет
|
||||
форматом целей и задач.
|
||||
9. `docs.py check` из скилла `canon` — до отсутствия дрейфа. Замечания о
|
||||
7. Каталог задач и первые цели — **вызови скилл `av-dev-tasks:tasks`**: он владеет
|
||||
форматом целей и задач. Не разрешился — учёт задач остаётся владельцу, и это
|
||||
тоже строка доклада.
|
||||
8. `docs.py check` из скилла `canon` — до отсутствия дрейфа. Замечания о
|
||||
незаполненных плейсхолдерах остаются: их закрывает не `init`, а работа.
|
||||
9. **Вычитай написанное — агент `doc-wording`**, по пачке заполненных документов
|
||||
(`passport.md`, `CLAUDE.md`, `security.md`). Здесь он нужен сильнее, чем где
|
||||
бы то ни было: весь текст сочинён только что и по свободному брифу человека, а
|
||||
бриф — это как раз залог, оценки без факта и жаргон. Скелеты с честной строкой
|
||||
в пачку не клади, вычитывать в них нечего. Находки — готовые формулировки,
|
||||
подставляешь их ты.
|
||||
10. Покажи человеку, что получилось, и **отдельным списком** — что выведено из
|
||||
брифа, что предположено, что осталось неизвестным. Правят по этим строкам.
|
||||
|
||||
@@ -98,7 +141,7 @@ description: "Завести новый проект — сессия вопро
|
||||
|
||||
- Содержимое канона по ходу разработки ведёт скилл `docs`.
|
||||
- Раскладку проверяет `canon check`.
|
||||
- Первую задачу берёт пайплайн проекта; `architecture.md` и `conventions/`
|
||||
- Первую задачу берёт конвейер проекта; `architecture.md` и `conventions/`
|
||||
наполняются его шагом синка, а не заранее.
|
||||
|
||||
## Чего этот скилл не делает
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"name": "av-dev-pipeline",
|
||||
"description": "Проведение задачи через полный цикл Spec Driven Development и конвейер ревью с детерминированным гейтом, сверкой со спеками, враждебными постановками, эксплуатационным постмортемом, независимой реализацией и обязательным триажем; плюс прогон нескольких задач разом по одной в изолированном worktree. Требует OpenSpec. Задача принимается и обычным текстом. Плагин av-dev-pm опционален: он даёт документы канона, из которых проходы читают проектную конкретику, и учёт задач; без него прогон деградирует поразрядно и называет это строкой.",
|
||||
"author": {
|
||||
"name": "Anton Vakhrushev",
|
||||
"email": "anwinged@gmail.com"
|
||||
}
|
||||
}
|
||||
@@ -1,487 +0,0 @@
|
||||
---
|
||||
name: task-batch
|
||||
description: Проводит несколько задач разом — планирует порядок и пересечения, гонит каждую задачу отдельным сабагентом в своём git worktree через task-pipeline (по умолчанию по одной задаче за раз; параллельно по графу зависимостей — по явной просьбе), интегрирует по одной ветке через rebase + fast-forward (линейная история), проверяет полноту ревью каждой ветки и в конце сверяет стыки, возникшие от слияния. Набор задач приходит извне. Использовать, когда просят сделать несколько задач сразу.
|
||||
---
|
||||
|
||||
# Батч задач
|
||||
|
||||
Оркестратор **набора** задач. Планирует порядок, раскидывает задачи по
|
||||
изолированным worktree, каждую проводит через полный цикл
|
||||
`av-dev-pipeline:task-pipeline`, затем сводит в основную ветку линейной историей
|
||||
и делает финальную сверку. Тонкая обёртка над пайплайном задачи — не
|
||||
переизобретай её шаги, вызывай как есть.
|
||||
|
||||
**По умолчанию задачи идут по одной**, в порядке зависимостей. Параллельно — по
|
||||
явной просьбе, и тогда параллельность **по графу зависимостей**: одновременно
|
||||
гонится только то, между чем нет ни зависимости, ни пересечения. Правило и его
|
||||
цена — в шаге 4.
|
||||
|
||||
Работай **максимально автономно**, по тому же принципу, что и одиночный пайплайн:
|
||||
вопрос, который решать не тебе, записывается и не останавливает поток; спрашиваем
|
||||
только про **необратимое** (деплой, выкладка наружу, удаление или перезапись
|
||||
рабочих данных). Механику — планирование, worktree, rebase, интеграцию, чистку —
|
||||
делаем без спроса.
|
||||
|
||||
## Предпосылки
|
||||
|
||||
- **OpenSpec и скиллы `opsx:*`** — на них стоит цикл внутри каждого сабагента и
|
||||
проход `review-specs` финальной сверки. Проекта без OpenSpec это касается так
|
||||
же, как одиночного пайплайна (см. его раздел «Предпосылки»).
|
||||
- **Скиллы зовутся с пространством имён**: `av-dev-pipeline:task-pipeline`,
|
||||
`av-dev-pipeline:review-pipeline`, `av-dev-pm:tasks`. Короткое имя
|
||||
может разрешиться в устаревшую проектную копию, и это произойдёт молча — в
|
||||
charter'е сабагента пиши полное имя, он твоего контекста не видит.
|
||||
- **Проектные копии этих скиллов и агентов при установке плагина удаляются.**
|
||||
|
||||
Перед стартом прочитай `CLAUDE.md` проекта: оттуда берутся **имя основной
|
||||
ветки** (оно подставляется в каждую команду git ниже), команда и семантика
|
||||
гейта, инварианты и что запускать запрещено. Раскладка нумерованных артефактов —
|
||||
`docs/database.md` и `docs/.pm.json` (ключ `migrations`).
|
||||
|
||||
**Документов канона нет — проект к нему не приведён.** Скажи это строкой и
|
||||
предложи `av-dev-pm:canon` **до первой задачи**: иначе каждая задача батча
|
||||
заплатит поразрядной деградацией ревью, а имя основной ветки придётся
|
||||
угадывать.
|
||||
|
||||
## Границы
|
||||
|
||||
- **Набор задач приходит извне.** Батч его не формирует: не выбирает из беклога,
|
||||
не приоритизирует, не решает, что важнее. Набор не задан — попроси его у
|
||||
вызывающего и остановись.
|
||||
- **Батч не владеет спринтом и целями.** Он сообщает исход по каждой задаче в тех
|
||||
же трёх словах, что и `task-pipeline`: сделана / не доведена / оказалась крупнее
|
||||
задачи.
|
||||
- **Задачи закрывает пайплайн внутри каждого сабагента**, шагом 12 — после
|
||||
коммита работы и **отдельным коммитом учёта**, вызовом Skill `av-dev-pm:tasks`.
|
||||
Батч сам записей учёта не трогает: он не знает, чем кончилась приёмка, и
|
||||
дублировать закрытие ему незачем. Но грязное дерево после сабагента — **его**
|
||||
проблема: на нём откажут и `rebase`, и `worktree remove` (см. шаг 6). Урожай ревью батч
|
||||
отдаёт списком, а задачи из него заводит тот, кто ведёт задачи проекта.
|
||||
|
||||
## Ключевое отличие от одиночного пайплайна
|
||||
|
||||
`task-pipeline` коммитит **в текущую ветку**, и при ручном запуске это основная
|
||||
ветка. Здесь так нельзя, поэтому батч — **осознанное исключение**: временные
|
||||
ветки и worktree заводятся лишь как средство изоляции, а конечное состояние — та
|
||||
же линейная история основной ветки через rebase + fast-forward. Ветки после
|
||||
вливания удаляются.
|
||||
|
||||
Изоляция нужна **в обоих режимах, а не только в параллельном**: батч не вливает
|
||||
ветку, пока не проверил полноту её ревью (шаг 5), и упавшая задача обязана
|
||||
остаться в своём worktree для ручного дожатия (шаг 6), не оставив следа в
|
||||
основной ветке. В параллельном режиме к этому добавляется вторая причина —
|
||||
задачи не должны видеть недоделанную работу друг друга.
|
||||
|
||||
## Модель исполнения
|
||||
|
||||
- Каждая задача = **один автономный сабагент** (`general-purpose`, чтобы иметь
|
||||
доступ к Skill и Agent для вложенных чекпоинтов ревью), работающий **только в
|
||||
своём worktree** и прогоняющий `task-pipeline` целиком на этой задаче.
|
||||
- Оркестратор кода задач не пишет: он планирует, заводит worktree, запускает
|
||||
сабагентов, проверяет полноту их ревью, интегрирует ветки и делает финальную
|
||||
сверку.
|
||||
- Стиль правок внутри — заточка под проект и конвенции, right-size, без
|
||||
золочения.
|
||||
|
||||
## Шаги
|
||||
|
||||
### 1. Прочитать набор
|
||||
|
||||
Набор задан списком (слаги, файлы, описания) — прочитай файл каждой задачи и
|
||||
связанные спеки и черновики. Сырьё (в терминах `av-dev-pm` — запись типа
|
||||
`research` с пустым разделом «Вопрос») включается, но помни: сабагент проведёт
|
||||
его сперва через `opsx:explore`, это тяжелее и чаще упирается в вопрос.
|
||||
|
||||
### 2. Спланировать порядок и пересечения (автономно)
|
||||
|
||||
Для каждой задачи определи:
|
||||
|
||||
- **затронутые capability** — по её описанию и по каталогу актуальных спек
|
||||
(`openspec/specs/`);
|
||||
- **жёсткие зависимости**: задача B строится на результате A → A строго раньше B;
|
||||
- **замеряющая задача** — та, чьё ревью будет доказывать находки **числами**. В
|
||||
последовательном прогоне это ничего не меняет: она и так идёт одна. В
|
||||
параллельном она гонится в волне **одна** (обоснование — ниже, в шаге 4).
|
||||
Помечается здесь, на планировании, а не во время прогона, и **независимо от
|
||||
режима**: состав волны определяется сейчас, режим может смениться просьбой уже
|
||||
после плана, а метка ревью назовёт разметчик уже внутри пайплайна задачи,
|
||||
после `propose`, — ключевать волну на ещё не сделанный выбор нельзя. Триггеры — по фактам о задаче, каждый
|
||||
сам по себе достаточен:
|
||||
- трогает схему хранилища, миграцию, формат на диске или объём хранимого;
|
||||
- трогает конкурентность: транзакции, блокировки, фоновые циклы, общее
|
||||
состояние;
|
||||
- трогает размер тела, буфер, память, сжатие, ретеншен, темп потока;
|
||||
- её тема названа в журнале `docs/review.md` как место, где уже ломалось.
|
||||
|
||||
Ни один триггер не сработал — задача не замеряющая, даже если её ревью
|
||||
окажется `large`. Метка про глубину проверки, замеряющая — про соревнование за
|
||||
железо; это разные вопросы, и совпадают они не всегда. Обратное тоже бывает и
|
||||
тоже законно: помеченная задача, чьё ревью пошло меткой `small` или
|
||||
`medium`, машину не займёт вовсе — меряющие проходы живут только в `large`.
|
||||
Пометка от этого не снимается: она ставится **до** разметки, и перестраховка
|
||||
здесь стоит одной волны, а ошибка — испорченных чисел;
|
||||
- **нумерованные артефакты — номера раздаёт оркестратор заранее.** Если проект
|
||||
нумерует миграции (путь — `docs/.pm.json`, ключ `migrations`), посмотри последний
|
||||
номер и **раздай номера всем задачам, которые, вероятно, их добавят**, до
|
||||
запуска. Номер уходит в charter сабагента, и он берёт назначенный, а не
|
||||
«следующий свободный».
|
||||
|
||||
**Это отдельная механика от правила волны, и она ему не служит** — их раньше
|
||||
путали, и они тянули в разные стороны. Правило волны отвечает на вопрос «кто с
|
||||
кем гонится одновременно», предраздача — на вопрос «какой номер берёт задача».
|
||||
Раздача нужна там, где **две задачи одной под-пачки** добавляют нумерованный
|
||||
артефакт: каждая считает «следующий свободный» по основной ветке, которая ещё
|
||||
не видела соседку, и обе берут один номер. Миграции под это почти не попадают —
|
||||
миграция и так триггер замеряющей задачи, а замеряющая идёт одна; но
|
||||
нумерованные артефакты бывают не только миграциями. Поэтому номера раздаются
|
||||
**всем** задачам с таким артефактом, независимо от того, в какой волне они
|
||||
окажутся: раздача ничего не стоит, а её отсутствие ловится только конфликтом на
|
||||
интеграции. Между волнами проблемы нет — ветка следующей волны берётся от
|
||||
вершины, уже включающей предыдущие; в последовательном прогоне проблемы нет по
|
||||
той же причине, но номера всё равно раздай: режим может смениться просьбой, а
|
||||
раздача бесплатна;
|
||||
- **жёстко сериализуем** (не гоняем одновременно) настоящие пересечения:
|
||||
- **одна capability на несколько задач** — две задачи, правящие одну спеку (тем
|
||||
более одно и то же `### Requirement`), дают не текстовый, а **семантический**
|
||||
конфликт при архивации; сериализуем по смыслу, а не только по файлам;
|
||||
- пересечение по одним и тем же исходникам;
|
||||
- **мягкие конфликты** сериализовать не надо: файлы-перечни, где каждая задача
|
||||
правит **свою** строку (индексы, оглавления, списки записей), и спеки разных
|
||||
capability — разные строки и файлы, сливаются сами.
|
||||
|
||||
Собери план как **граф зависимостей**, а не как плоский список: рёбра — жёсткие
|
||||
зависимости и сериализуемые пересечения. Дальше по режиму:
|
||||
|
||||
- **последовательно (умолчание)** — линеаризуй граф в один порядок:
|
||||
топологический, а там, где он оставляет свободу, — раньше то, от чего зависит
|
||||
больше задач, и раньше то, что правит общие для набора места. Волн нет,
|
||||
замеряющие задачи ничем не отличаются от прочих;
|
||||
- **параллельно (по просьбе)** — нарежь граф на **волны**: в одну волну попадают
|
||||
только задачи, между которыми нет ребра; замеряющие стоят отдельными волнами по
|
||||
одной.
|
||||
|
||||
Рёбра у графа двух видов, и путать их не надо: **зависимость** направлена (B без
|
||||
результата A не делается), **пересечение** — нет (кто первый, неважно, лишь бы не
|
||||
разом). Тот же словарь у графа проходов ревью — см. «Порядок прогона» в
|
||||
`av-dev-pipeline:review-pipeline`.
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A["A: схема хранилища<br/>(замеряющая)"]
|
||||
B["B: эндпоинт поверх A"]
|
||||
C["C: формат лога"]
|
||||
D["D: правит ту же capability, что C"]
|
||||
|
||||
A -->|зависимость| B
|
||||
C -. пересечение — одна capability .- D
|
||||
```
|
||||
|
||||
Этот граф даёт: **последовательно** — `A → B → C → D` (или `A → C → B → D`, обе
|
||||
линеаризации законны); **параллельно** — волна 1 `A` одна (замеряющая), волна 2
|
||||
`B` и `C`, волна 3 `D`.
|
||||
|
||||
Схемы в этом скилле — **пример и сводка**, правила ставит текст: при расхождении
|
||||
прав он. (В `av-dev-pipeline:review-pipeline` наоборот — там граф прогона и есть
|
||||
алгоритм, и старший он.)
|
||||
|
||||
Покажи план короткой репликой — режим, порядок или состав волн, какие задачи
|
||||
признаны замеряющими и по какому триггеру, — и иди дальше.
|
||||
|
||||
### 3. Свежая база
|
||||
|
||||
Убедись, что рабочее дерево чистое и основная ветка свежая. Зафиксируй базовый
|
||||
коммит. Новые ветки бери от свежей вершины; ветку следующей задачи (в
|
||||
параллельном режиме — ветки следующей волны) — от вершины, уже включающей
|
||||
результат предыдущих.
|
||||
|
||||
### 4. Провести задачи
|
||||
|
||||
**Умолчание — по одной задаче за раз, в порядке из шага 2.** Следующая стартует,
|
||||
когда предыдущая вернула отчёт и (если она зелёная) влилась. Обосновывать это не
|
||||
надо — обосновывается отступление. Причина умолчания в том, что задача батча
|
||||
дороже прохода ревью: каждая тянет полный цикл пайплайна с гейтом, поднятием
|
||||
сервиса и вложенным ревью, и две такие на одной машине дерутся за порты, рабочие
|
||||
каталоги, СУБД и само железо. Последовательный прогон к тому же оставляет ревью
|
||||
внутри задачи его собственное умолчание — **параллельные проходы**: машина
|
||||
свободна, и выигрыш берётся там, где он ничего не стоит.
|
||||
|
||||
**Параллельно — по явной просьбе, и параллельность идёт по графу зависимостей.**
|
||||
Одновременно гонится только то, между чем на шаге 2 не нашлось ребра: ни жёсткой
|
||||
зависимости, ни общей capability, ни общих исходников. «Гони параллельно» не
|
||||
означает «гони всё разом» — граф остаётся в силе, просьба лишь разрешает
|
||||
использовать его ширину.
|
||||
|
||||
В параллельном режиме действуют два ограничения:
|
||||
|
||||
- **потолок — 2–3 задачи одновременно.** Больше трёх разом душат машину и
|
||||
провоцируют гонки. Волну шире трёх бей на под-пачки по ≤3 и **гони под-пачки
|
||||
последовательно**: следующая стартует, когда предыдущая вернула отчёты. Иначе
|
||||
потолок обходится тривиально — шесть задач, запущенных «двумя под-пачками» в
|
||||
одном сообщении, это шесть задач разом;
|
||||
- **волна из одной задачи обязательна для замеряющей.** Задача, признанная на
|
||||
шаге 2 **замеряющей**, гонится одна: соседний прогон на той же машине портит
|
||||
числа, а находка с испорченным оракулом хуже отсутствующей — она выглядит
|
||||
доказанной. Если замеряющая задача всё же пошла в общей волне, её отчёт обязан
|
||||
нести строку в границах покрытия: замеры сняты под соседней нагрузкой.
|
||||
|
||||
Для каждой задачи (в параллельном режиме — для каждой задачи под-пачки):
|
||||
|
||||
1. Заведи worktree и ветку от текущей вершины:
|
||||
`git worktree add <path> -b task/<slug> <основная ветка>`. Путь — во временном
|
||||
каталоге проекта (`./tmp`), не в системном `/tmp`.
|
||||
2. Запусти сабагента, `subagent_type: general-purpose`: в последовательном режиме
|
||||
— одного и дождись отчёта; в параллельном — **по одному на задачу под-пачки,
|
||||
всех в одном сообщении**. Charter сабагента:
|
||||
- работай **строго в своём worktree** `<path>`; в другие каталоги и в основную
|
||||
ветку не лезь;
|
||||
- прогони Skill **`av-dev-pipeline:task-pipeline`** ровно на этой задаче,
|
||||
полный цикл SDD с обоими чекпоинтами ревью;
|
||||
- если задаче назначен **номер артефакта** — используй строго его;
|
||||
- **метка ревью выбирает разметчик конвейера, а не ты и не сабагент.** Он
|
||||
идёт внутри пайплайна задачи, шагом 4, сразу после `propose`, и его план
|
||||
обслуживает оба чекпоинта. Метка в вызов не передаётся вовсе. Батч не
|
||||
повод её понижать: «нас много и
|
||||
мы спешим» — ровно тот стимул, из-за которого проходы пропускают, и он снят
|
||||
тем, что регулятор не в руках у автора;
|
||||
- **режим прогона проходов ревью — от режима батча**, и его называет charter,
|
||||
а не сабагент: батч идёт по одной задаче → режим умолчательный, **`по
|
||||
графу`** (машина свободна); батч идёт волнами → **`линейно`**, твой worktree
|
||||
не один на машине, и этой причиной ты обязан объяснить режим в отчёте.
|
||||
Внутренние рёбра графа — цепочку проходов, держащих машину — конвейер
|
||||
соблюдает сам, в любом режиме;
|
||||
- **если вложенные сабагенты недоступны** (движок не даёт запускать агентов из
|
||||
агента) — не пропускай ревью и не понижай метка: проведи его **инлайн** по
|
||||
тем же charter'ам `av-dev-pipeline`, сохранив обязательное — разметку первой
|
||||
(план с темами и меткой), гейт до опиниативных проходов, состав по плану,
|
||||
триаж последним. И **скажи в отчёте прямым текстом, что ревью шло инлайн**:
|
||||
инлайновый проход видит контекст автора и потому разведён с ним слабее — а
|
||||
инлайновая разметка вдобавок означает, что метка выбрал автор, и это
|
||||
отдельная строка;
|
||||
- **вернуть отчёт**, в котором обязательно: исход задачи одним из трёх слов;
|
||||
**план прогона: метка с обоснованием, темы и их глубины**, и режим; что сделано; какие вопросы
|
||||
записаны и куда; изменённые файлы; добавлялся ли нумерованный артефакт и с
|
||||
каким номером; затронутые capability; состояние гейта; **перечень
|
||||
тем с исходом по каждой**; **путь к
|
||||
сохранённому отчёту триажа** (`openspec/changes/<id>/review/`, после
|
||||
архивации — `openspec/changes/archive/<id>/review/`); шло ли ревью
|
||||
инлайн; границы покрытия.
|
||||
|
||||
**Шаги 5 и 6 отрабатываются на вернувшейся задаче до старта следующей** — в
|
||||
параллельном режиме на вернувшейся волне до старта следующей. Иначе ветка
|
||||
следующей возьмётся от вершины, не видевшей предыдущую работу, и весь смысл
|
||||
порядка из шага 2 теряется.
|
||||
|
||||
Сабагент, упершийся в вопрос, **не останавливает батч**: он записывает вопрос,
|
||||
режет задачу до остатка и доводит остаток — либо, если остатка нет, возвращает
|
||||
исход «не доведена». Оркестратор собирает такие вопросы и выносит их в финальный
|
||||
доклад пачкой.
|
||||
|
||||
### 5. Проверить полноту ревью — до интеграции
|
||||
|
||||
**Ветка, чей отчёт не называет план прогона, не вливается.** Пропуск не отличим
|
||||
от прохода без находок, и на уровне батча это ещё опаснее: отчётов много, каждый
|
||||
выглядит полным, а сверять их некому, кроме тебя.
|
||||
|
||||
Сверка идёт в три шага, и порядок важен:
|
||||
|
||||
1. **Возьми план прогона** из отчёта задачи — таблицу «тема → дом → глубина → кто
|
||||
закрывает» с меткой и обоснованием; он затем и заказан в обязательных полях
|
||||
шага 4. Плана в отчёте нет — сверять не с чем; это само по себе основание не
|
||||
вливать, пока сабагент не покажет план разметки задачи.
|
||||
2. **Сверяй с независимым артефактом, а не с прозой отчёта.** Перечень проходов
|
||||
бери из **сохранённого отчёта триажа** (`openspec/changes/<id>/review/` или
|
||||
`openspec/changes/archive/<id>/review/` — задача доведена, change заархивирован) —
|
||||
пайплайн обязан его туда положить. Проза сабагента написана тем же, кто мог
|
||||
проход и пропустить: она подтверждает сама себя. Отчёта триажа на месте нет —
|
||||
считай, что состав неизвестен, и дозапускай ревью целиком.
|
||||
3. **Сверь план с исходом**: против каждой темы плана обязан стоять отчёт либо
|
||||
названная причина его отсутствия. Раскладка «тема → кто закрывает с этой меткой» — в скилле `av-dev-pipeline:review-pipeline`.
|
||||
|
||||
Расхождение — не повод отменять задачу: дозапусти недостающие проходы **на
|
||||
ветке**, в её worktree, через `av-dev-pipeline:review-pipeline`, и только потом
|
||||
интегрируй.
|
||||
|
||||
**Передай в дозапуск тот же план.** Триаж требует его обязательным входом — без
|
||||
плана он не может сверить, все ли размеченные темы вернули отчёт, а эта сверка и
|
||||
есть то, ради чего дозапуск затевается. Плана не осталось (сабагент не сохранил
|
||||
его в отчёте) — пусть повторит разметку задачи: это самый дешёвый проход
|
||||
конвейера, и он дешевле, чем прогон, который нечем сверить.
|
||||
|
||||
**Находки дозапуска — такие же находки, и зелёный гейт их не отменяет.** Правило
|
||||
интеграции «вливаем только зелёные» смотрит на гейт, а дозапущенный `critical`
|
||||
гейт не красит: он был бы пропущен молча, если это не сказать прямо. Поэтому:
|
||||
|
||||
- `critical` или `major` из дозапуска — **вливание этой ветки останавливается**.
|
||||
Помеченное `инлайн` чинится в её worktree, после починки — гейт, затем
|
||||
интеграция. Помеченное `развилка` — вопрос в запись, задача режется до остатка
|
||||
ровно так же, как это сделал бы пайплайн внутри;
|
||||
- остатка нет — ветка не вливается и уходит в доклад как провалившаяся, со своим
|
||||
worktree;
|
||||
- `minor` и `nit` из дозапуска — в урожай доклада, вливанию не мешают.
|
||||
|
||||
Отчёт дозапуска приложи к отчёту задачи и назови в докладе (шаг 9), почему он
|
||||
понадобился: систематический пропуск одного и того же прохода — находка о самом
|
||||
конвейере, а не о задаче.
|
||||
|
||||
### 6. Интегрировать — rebase + fast-forward, по одной ветке
|
||||
|
||||
Сводим ветки **строго последовательно** (линейная история), в порядке
|
||||
зависимостей. Вливаем **только зелёные**.
|
||||
|
||||
**Ветка задачи занята её worktree, и это определяет форму команд.** Пока worktree
|
||||
жив (а удаляется он последним, после зелёного гейта), ветка `task/<slug>`
|
||||
checkout'нута в нём, и `git rebase <основная> task/<slug>` из главного worktree
|
||||
**падает**: `fatal: 'task/<slug>' is already used by worktree at …`. Поэтому
|
||||
rebase делается **внутри worktree задачи**, а ff-слияние — из главного.
|
||||
|
||||
Для каждой готовой ветки `task/<slug>`:
|
||||
|
||||
- `git -C <path> rebase <основная>` — перенос ветки задачи на текущую вершину,
|
||||
выполняется в её собственном worktree;
|
||||
- резолв конфликтов (их почти нет — конфликтоопасное сериализовано, номера
|
||||
розданы заранее). Неавтоматический конфликт — **не форсируй**: прерви
|
||||
(`git -C <path> rebase --abort`), оставь ветку и worktree как есть, вынеси это
|
||||
в доклад как нераспознанное пересечение;
|
||||
- **дерево сабагента обязано быть чистым.** `git -C <path> status --porcelain`
|
||||
до `rebase`: непусто — значит сабагент не довёл шаг 12 до коммита учёта (или
|
||||
оставил мусор). Не форсируй и не коммить за него: назови задачу в докладе
|
||||
недоведённой и оставь ветку с worktree. Молчаливый `rebase` на грязном дереве
|
||||
всё равно откажет, но с сообщением про unstaged changes — а причина другая;
|
||||
- **ненулевой код `rebase` относится к этой ветке и только к ней.** Прерванный
|
||||
rebase в чужом worktree не трогает ни главное дерево, ни остальные ветки:
|
||||
проверь `git -C <path> status` и `git status` — обе чистые. Уводить весь батч
|
||||
в провалившиеся из-за одного ненулевого кода запрещено: это ложная причина,
|
||||
из-за которой зелёные задачи не доедут до основной ветки. Провалилась одна —
|
||||
провалилась одна;
|
||||
- из главного worktree (он стоит на основной ветке — проверь
|
||||
`git rev-parse --abbrev-ref HEAD`): `git merge --ff-only task/<slug>`. Ветку в
|
||||
главном дереве **не переключай** — `git checkout task/<slug>` тоже упрётся в
|
||||
занятость;
|
||||
- после каждой интеграции — **гейт на основной ветке**. Красное — **откати эту
|
||||
интеграцию** (`git reset --hard` на прошлую вершину), ветку с worktree сохрани,
|
||||
задачу перечисли в докладе. Основная ветка **никогда** не остаётся
|
||||
полузелёной;
|
||||
- только после зелёного: `git worktree remove <path>` и
|
||||
`git branch -d task/<slug>` — в этом порядке, иначе ветка снова занята.
|
||||
|
||||
**Политика частичного провала.** Упавшая задача (исход «не доведена», красные
|
||||
тесты в её worktree, конфликт при rebase, невлитая из-за находок дозапуска)
|
||||
**не блокирует остальные**: интегрируем все зелёные, упавшую оставляем в её
|
||||
worktree и ветке нетронутой — ничего не удаляем, — и перечисляем в докладе с
|
||||
причиной, отчётом и путём к worktree. Причина называется **настоящая**: «конфликт
|
||||
rebase в файле X», а не «нераспознанное пересечение» на всякий случай.
|
||||
|
||||
### 7. Финальный гейт
|
||||
|
||||
На основной ветке после всех интеграций — гейт целиком. Зелёное обязательно; пока
|
||||
красное, шаг 8 не начинается.
|
||||
|
||||
### 8. Финальная сверка — только то, чего не видел никто
|
||||
|
||||
Каждая задача уже прошла полный конвейер в своём worktree. Повторять его на
|
||||
интегрированном диффе бессмысленно: те же проходы на тех же файлах дадут те же
|
||||
находки и удорожат триаж. Здесь проверяется **только то, что появилось от
|
||||
слияния**:
|
||||
|
||||
- запусти **по одному `review-specs` на каждую затронутую capability**, все
|
||||
разом — граф здесь плоский: машину эти проходы не держат, ребра между ними нет,
|
||||
а сама машина к этому моменту свободна (все сабагенты вернулись). Линейно —
|
||||
только по причине из раздела «Порядок прогона»
|
||||
`av-dev-pipeline:review-pipeline`, и причину назови;
|
||||
- **задание у этих проходов особое, и это надо сказать прямо.** Живого change
|
||||
здесь нет — все заархивированы, дельта-спек не существует. Источник требований
|
||||
— **актуальные** `openspec/specs/<capability>/spec.md`, а предмет — стык:
|
||||
требование, которое одна задача выполнила, а соседняя незаметно отменила; два
|
||||
архивных change, по-разному описавшие одно поведение. Скажи проходу это прямо,
|
||||
иначе он пойдёт искать дельты и не найдёт ничего;
|
||||
- если задачи пересекались по файлам, добавь один `review-architecture` на
|
||||
интегрированный дифф с вопросом «не появился ли второй способ делать то, что
|
||||
уже делается» — именно он возникает, когда две задачи независимо решали
|
||||
похожее;
|
||||
- **заверши триажем.** Он единственный, кто агрегирует, и без него у находок нет
|
||||
ни оракула, ни пометки `инлайн`/`развилка` — а следующий абзац на неё
|
||||
опирается. Прогон из двух проходов без триажа — это сырые находки, выданные за
|
||||
разобранные;
|
||||
- **план триажу собери сам, здесь, — разметчика на этой сверке нет.** Триаж
|
||||
требует план обязательным входом: он сверяет размеченное с пришедшим, и без
|
||||
плана эта сверка не выполняется вовсе. Разметка задачи сюда не годится — она
|
||||
описывала одну задачу, а сверка идёт по интегрированной ветке. План здесь
|
||||
короткий и составляется по факту запуска:
|
||||
|
||||
```
|
||||
метка: не применяется — сверка стыка, а не ревью изменения
|
||||
тема дом глубина закрывает
|
||||
requirements openspec/specs/<capability-1>/ разбор specs (стык)
|
||||
requirements openspec/specs/<capability-2>/ разбор specs (стык)
|
||||
architecture docs/architecture.md разбор architecture
|
||||
+ источник docs/passport.md
|
||||
```
|
||||
|
||||
Темы, которых в этом списке нет (`autotests`, `conventions`, `security`,
|
||||
`operations`, свои темы проекта), назови строкой «не проверяется на сверке
|
||||
стыка: закрыто прогонами отдельных задач». Это не формальность — без такой
|
||||
строки отчёт сверки читается как полное ревью ветки.
|
||||
|
||||
Граф этой сверки — веер в один сток, и он такой же, как у обычного прогона:
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
merged["основная ветка после всех интеграций<br/>(финальный гейт зелёный)"]
|
||||
s1["review-specs: capability 1<br/>режим «стык после слияния»"]
|
||||
s2["review-specs: capability 2<br/>режим «стык после слияния»"]
|
||||
arch["review-architecture на интегрированном диффе<br/>(если задачи пересекались по файлам)"]
|
||||
tri["review-triage — сток"]
|
||||
|
||||
merged --> s1 --> tri
|
||||
merged --> s2 --> tri
|
||||
merged --> arch --> tri
|
||||
```
|
||||
|
||||
Замечания отрабатывай как одиночный пайплайн: `инлайн` чини сам, `развилка` —
|
||||
вопросом в запись; после правок — снова гейт.
|
||||
|
||||
### 9. Прибраться и доложить
|
||||
|
||||
- Убери worktree и ветки **только успешно влитых** задач, в конце
|
||||
`git worktree prune`. Worktree и ветки **провалившихся** не трогай — они нужны
|
||||
для ручного дожатия.
|
||||
- **Записей учёта батч не трогает** — их правит пайплайн внутри сабагента на
|
||||
шаге 12. Батч сообщает исход по каждой задаче; если какой-то сабагент дошёл до
|
||||
коммита, но закрытия не сделал (плагина нет, вызов не разрешился), скажи это
|
||||
строкой — иначе задача останется открытой молча.
|
||||
- Доложи кратко:
|
||||
- **исход по каждой задаче** одним из трёх слов, с хешем коммита;
|
||||
- **режим прогона** — по одной или волнами, и если волнами, то по чьей просьбе;
|
||||
порядок задач или состав волн, порядок интеграции, с пометкой, какие задачи
|
||||
шли по одной как замеряющие;
|
||||
- вопросы, записанные сабагентами, пачкой;
|
||||
- что дозапускалось на шаге 5 и почему; шло ли где-то ревью инлайн;
|
||||
- итог финальной сверки и ссылки на архивные change;
|
||||
- **`Урожай`** — отложенные находки всех задач одним списком, с провенансом.
|
||||
Задачи из него заводит тот, кто ведёт задачи проекта, а не батч;
|
||||
- **отдельно — провалившиеся** задачи с настоящей причиной и путём к
|
||||
оставленному worktree;
|
||||
- **границы покрытия сводной строкой**, включая задачи, чьи замеры снимались в
|
||||
общей волне, и ветки, где ревью шло инлайн.
|
||||
|
||||
## Тонкости
|
||||
|
||||
- **Изоляция параллельных тестов — цена параллельного режима, и проверяется она
|
||||
до первой волны.** Прежде чем гнать несколько прогонов разом, убедись, что
|
||||
тесты не делят фиксированный порт или файл БД (обычно берут временный каталог и
|
||||
эфемерный порт — тогда ок). Делят — такие задачи гони по одной, даже если
|
||||
просили параллельно, и скажи об этом строкой: просьба про параллельность, а не
|
||||
про сломанные тесты. В умолчательном режиме вопрос не встаёт вовсе — это одна
|
||||
из причин, по которым умолчание такое.
|
||||
- Поведенческая верификация внутри сабагента поднимает изменение вживую: в
|
||||
параллельном режиме следи, чтобы соседние worktree не дрались за порты и
|
||||
рабочие каталоги. Если проект умеет поднимать только один экземпляр — такие
|
||||
задачи в одну волну не ставь.
|
||||
- Ревью выполненного — **до** интеграции; это забота
|
||||
`av-dev-pipeline:task-pipeline` внутри каждого сабагента, дублировать не надо.
|
||||
- `openspec validate --strict` тоже внутри пайплайна задачи — не пропускай его
|
||||
своими правками на интеграции.
|
||||
- Крупная переработка, предложенная ревью внутри задачи, — развилка: не вливай
|
||||
молча, вынеси в доклад.
|
||||
- Держи вызывающего в цикле короткими репликами на переходах фаз (план → прогон
|
||||
задач → интеграция → финальная сверка), но не проси подтверждать механику.
|
||||
@@ -1,478 +0,0 @@
|
||||
---
|
||||
name: task-pipeline
|
||||
description: "Автономно проводит одну задачу через полный цикл Spec Driven Development — от постановки до коммита (opsx explore→propose→разметка задачи→ревью дизайна→apply→ревью кода→archive→коммит), с обязательными чекпоинтами ревью и докладом об исходе. Разметка идёт один раз, сразу после propose: она называет размер, сложность и метка, и её план определяет состав обеих стадий ревью. Использовать, когда просят взять/сделать задачу или довести идею до реализации."
|
||||
---
|
||||
|
||||
# Пайплайн задачи
|
||||
|
||||
Оркестратор **одной** задачи по Spec Driven Development: проводит её от
|
||||
постановки до коммита максимально автономно. Механику не согласовываем — делаем.
|
||||
|
||||
Это тонкая обёртка над каноническими скиллами `opsx:explore` / `opsx:propose` /
|
||||
`opsx:apply` / `opsx:archive` — вызывай их через Skill, не переизобретай их шаги.
|
||||
Ревью — скилл `av-dev-pipeline:review-pipeline`; он же держит правило выбора
|
||||
метки, а называет её агент `review-scope` на шаге 4 — один раз на задачу, для
|
||||
обеих стадий ревью.
|
||||
|
||||
## Предпосылки
|
||||
|
||||
- **OpenSpec и скиллы `opsx:*` — жёсткая предпосылка, а не опция.** На них стоят
|
||||
шаги 2, 3, 7 и 9, проход `review-specs` и ревью дизайна (они завязаны на
|
||||
`openspec/changes/<id>/specs/*/spec.md` и на `openspec validate --strict`).
|
||||
**Проект без OpenSpec этим пайплайном не ведётся** — подключай OpenSpec, а не
|
||||
вырождай цикл: ветка деградации здесь не пишется, потому что непроверенная
|
||||
ветка деградации хуже честного отказа.
|
||||
- **Скиллы зовутся с пространством имён** — `av-dev-pipeline:review-pipeline`,
|
||||
`av-dev-pm:docs`, `av-dev-pm:tasks`. Короткое имя может разрешиться в
|
||||
устаревшую проектную копию, и это произойдёт молча.
|
||||
- **Проектные копии этих скиллов и агентов удаляются при установке плагина**
|
||||
(`.claude/skills/` — и голые имена `task-pipeline`, `review-pipeline`,
|
||||
`task-batch`, и с префиксом проекта: `<проект>-task-pipeline`,
|
||||
`<проект>-review-pipeline`;
|
||||
`.claude/agents/<проект>-review-*.md`). Две копии одного скилла расходятся, и
|
||||
побеждает та, что короче названа.
|
||||
|
||||
Перед стартом прочитай `CLAUDE.md` проекта и то, на что он ссылается, если ещё
|
||||
не в контексте. Проектные факты, нужные ревью — инварианты, семантика гейта,
|
||||
объёмы, модель угроз, прецеденты, — живут в **документах канона** `av-dev-pm`;
|
||||
карта «что где» — `references/project-facts.md` конвейера ревью.
|
||||
|
||||
**Документов канона нет — проект к нему не приведён.** Скажи это строкой и
|
||||
предложи скилл `av-dev-pm:canon`: одна операция на проект против поразрядной
|
||||
деградации на каждой задаче. Работу при этом не останавливай.
|
||||
|
||||
## Границы: чем пайплайн не владеет
|
||||
|
||||
- **Беклогом, спринтом, целями и приоритетами.** Задача приходит извне. Пайплайн
|
||||
её не выбирает, не приоритизирует, не заводит и не переоценивает; если в
|
||||
проекте есть свой процесс управления задачами — он и решает, что брать.
|
||||
- **Форматом задач.** Пайплайн **не правит индексы руками и не выдумывает путь
|
||||
к скрипту учёта**: он зовёт Skill `av-dev-pm:tasks`, который этим владеет
|
||||
(шаг 12). Закрытие как таковое — его работа, и это осознанное решение с
|
||||
названной ценой: **приёмщик и исполнитель совпали**. Закрытие поэтому **не
|
||||
окончательно** — человек на сессии возвращает задачу `reopen` с причиной, а
|
||||
доклад по критериям приёмки становится единственным, по чему приёмка вообще
|
||||
возможна. Плагина `av-dev-pm` в проекте нет — вызов не разрешится, и тогда
|
||||
учёт остаётся владельцу, о чём говорится в докладе.
|
||||
- **Заведением задач из урожая ревью.** Отложенные находки отдаются **списком**
|
||||
(см. шаг 8); превращать их в задачи — работа того, кто ведёт задачи проекта.
|
||||
- **Определением ценности.** «Нужна ли эта функциональность» — не вопрос
|
||||
пайплайна ни на одном шаге.
|
||||
|
||||
Пайплайн владеет **своим** определением готовности (ниже) и **сообщает
|
||||
наблюдаемый исход**. Что с исходом делать дальше — не его дело.
|
||||
|
||||
## Наблюдаемые исходы
|
||||
|
||||
Ровно три, и каждый обязан быть назван в докладе прямо:
|
||||
|
||||
- **сделана** — определение готовности выполнено целиком;
|
||||
- **не доведена** — с причиной и с записанным вопросом; что именно сделано и до
|
||||
какой границы, названо явно;
|
||||
- **оказалась крупнее задачи** — распознаётся **до заведения change**, иначе его
|
||||
придётся выбрасывать. Дальше — декомпозиция, и это не работа пайплайна.
|
||||
|
||||
## Определение готовности
|
||||
|
||||
Задача сделана, когда верно всё:
|
||||
|
||||
1. гейт проекта зелёный;
|
||||
2. ревью проведено, **план прогона сверен с исходом по каждой теме**, темы без
|
||||
отчёта и без дома названы в границах покрытия;
|
||||
3. change заархивирован, дельты влиты в актуальные спеки;
|
||||
4. коммит сделан в текущую ветку;
|
||||
5. **критерии приёмки, если проект их дал, выписаны поимённо, и по каждому назван
|
||||
оракул и наблюдаемый исход** — «прогнал вот это, увидел вот то». Это **доклад,
|
||||
а не сертификация: приёмка — не работа пайплайна.** Исполнитель, ставящий себе
|
||||
галочку «принято», проверяет свою работу своим же взглядом — по границе это
|
||||
может делать только приёмщик, разведённый с исполнителем. Критерии приходят снаружи;
|
||||
пайплайн их не сочиняет и не занижает. Расхождение «по каждому критерию исход
|
||||
есть, а суть задачи не достигнута» — дефект критериев, и о нём сообщается, а
|
||||
не молча дорабатывается.
|
||||
|
||||
Пункты 1–4 — своё. Пункт 5 — внешнее: пайплайн доводит его до наблюдаемого
|
||||
исхода и передаёт дальше.
|
||||
|
||||
## Принцип автономности
|
||||
|
||||
**Умолчание — делать, а не спрашивать.** Задача доводится до коммита без участия
|
||||
человека; предполагается, что так пройдёт большинство задач.
|
||||
|
||||
Наткнулся на вопрос, который решать не тебе, — **не останавливайся и не
|
||||
спрашивай**. Запиши его и продолжай:
|
||||
|
||||
1. **Запиши вопрос там, где проект держит вопросы** (секция беклога, файл
|
||||
задачи, трекер — это знает проект). Если проект не сказал, куда, — отдельной
|
||||
секцией `Вопросы` в своём докладе, и это тоже исход. Тело отвечает на три
|
||||
вещи: **что именно решить**, **какие есть варианты и цена каждого**, **что
|
||||
стоит, пока решения нет**. Плюс твоя рекомендация — человек чаще соглашается,
|
||||
чем выбирает заново, и готовое суждение экономит ему весь контекст.
|
||||
2. **Переформулируй задачу на остаток** — то, что делается без этого решения.
|
||||
Назови границу: докуда доводим сейчас.
|
||||
3. Доведи остаток до конца и закоммить. Задача не «висит на вопросе», она сделана
|
||||
в объявленных границах.
|
||||
|
||||
**Что остатком не является — правило живёт не здесь.** Канонический текст с обеими
|
||||
оговорками — в плагине `av-dev-pm`, скилл `av-dev-pm:session`, раздел
|
||||
`## Вопрос, блокер, необратимое`, подраздел «Отличать вопрос от застревания».
|
||||
Правило принадлежит управлению задачами, потому что решает **сделана задача или
|
||||
вышла**, — это исход планирования, а не исполнения. **Ссылайся, не
|
||||
пересказывай:** копия, заведённая здесь, уже однажды разошлась с оригиналом и
|
||||
потеряла из перечня самое необратимое — запись **наружу**.
|
||||
|
||||
Коротко, чтобы знать, когда идти читать: остаток проверяется двумя порогами —
|
||||
**материализация нерешённого** (запись состояния, зависящего от неотвеченного
|
||||
вопроса) и **пол по пользе** (из остатка пропала польза, названная в постановке).
|
||||
Оба порога — стоп: первый поднимает решение до начала записи, второй даёт исход
|
||||
«не доведена».
|
||||
|
||||
Плагин `av-dev-pm` не подключён — правило не отменяется, а становится
|
||||
осторожнее: прежде чем записать зависящее от нерешённого куда бы то ни было —
|
||||
в хранилище, в журнал, в витрину или наружу, — спрашивай человека.
|
||||
|
||||
Нет полезного остатка — задача заканчивается исходом «не доведена», вопрос
|
||||
записан, ничего не коммитится наполовину.
|
||||
|
||||
### Когда всё-таки спрашивать
|
||||
|
||||
Узко и по другому основанию — не «сложное решение», а **необратимое действие**:
|
||||
|
||||
- деплой, выкладка наружу, смена публичного адреса или токенов;
|
||||
- удаление или перезапись рабочих данных, включая подрезку архивов;
|
||||
- всё, что уходит за пределы машины.
|
||||
|
||||
Здесь ошибка не откатывается коммитом, поэтому спрашиваем даже когда решение
|
||||
кажется очевидным. Развилка в дизайне — вопрос в запись; необратимое действие —
|
||||
вопрос человеку сейчас.
|
||||
|
||||
Стиль правок — заточка под проект и конвенции, right-size, без золочения.
|
||||
|
||||
## Шаги
|
||||
|
||||
Двенадцать шагов с одной развилкой и одним досрочным исходом:
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
s1["1. Прочитать задачу<br/>критерии приёмки выписать сразу"]
|
||||
triv{"тривиальная?"}
|
||||
big["исход «оказалась крупнее задачи»<br/>объявляется ДО заведения change"]
|
||||
s2["2. opsx:explore — груминг идеи"]
|
||||
s3["3. opsx:propose — change, дельта-спеки, tasks.md"]
|
||||
s4["4. разметка задачи — review-scope:<br/>размер, сложность, метка, план тем"]
|
||||
s5["5. ревью дизайна, состав по метке"]
|
||||
s6["6. отработать замечания + validate --strict"]
|
||||
s7["7. opsx:apply — код, гейт, поведенческая верификация"]
|
||||
s8["8. ревью кода, состав по той же метки"]
|
||||
s9["9. opsx:archive"]
|
||||
s10["10. синк документации — av-dev-pm:docs"]
|
||||
s11["11. коммит работы — av-dev-git:commit"]
|
||||
s12["12. закрыть задачу — av-dev-pm:tasks,<br/>вторым коммитом учёта"]
|
||||
|
||||
s1 --> triv
|
||||
s1 -.-> big
|
||||
triv -->|"нет: идея или мутная постановка"| s2
|
||||
s2 --> s3
|
||||
triv -->|"да: шаг 2 пропускается"| s3
|
||||
s3 --> s4 --> s5 --> s6 --> s7 --> s8 --> s9 --> s10 --> s11 --> s12
|
||||
s4 -.->|"план задачи: та же метка"| s8
|
||||
```
|
||||
|
||||
**Разметка стоит одна и обслуживает обе стадии ревью** — шаги 5 и 8. Это и есть
|
||||
пунктирное ребро на схеме: план, посчитанный на шаге 4, доезжает до ревью кода
|
||||
без пересчёта. Раньше разметка была первым проходом внутри шага ревью кода, а
|
||||
состав ревью дизайна называл сам пайплайн — то есть одна и та же величина
|
||||
считалась дважды, и один из двух раз тем, кто только что написал предложение.
|
||||
|
||||
Два чекпоинта ревью — шаги 5 и 8 — единственные места, где зовётся конвейер;
|
||||
порядок «сперва коммит работы, потом коммит учёта» на схеме тоже ребро, и оно
|
||||
обязательное (шаг 12).
|
||||
|
||||
Схема — **сводка**: содержание каждого шага в его разделе ниже, и при
|
||||
расхождении прав текст.
|
||||
|
||||
### 1. Прочитать задачу
|
||||
|
||||
Задача задана извне (slug, файл, ссылка, описание) — прочитай её и связанные
|
||||
спеки и черновики. Не задана — попроси у вызывающего; сам в беклог не лезь и
|
||||
приоритеты не интерпретируй.
|
||||
|
||||
Если проект даёт задаче **критерии приёмки**, выпиши их сразу: на шаге 3 они
|
||||
уезжают в `tasks.md` change. Файл задачи может быть удалён до коммита, а
|
||||
критерии обязаны его пережить.
|
||||
|
||||
Оцени тривиальность — **теперь она влияет ровно на один шаг, второй**:
|
||||
|
||||
- **тривиальная** — локальная правка без изменения поведения, спек и схемы,
|
||||
решение очевидно. Explore пропускается;
|
||||
- **нетривиальная** — новое или изменённое поведение, дизайн-развилки, задеты
|
||||
инварианты, схема или несколько capability. Полный цикл.
|
||||
|
||||
**На состав ревью тривиальность больше не влияет** — это работа шага 4. Раньше
|
||||
она решала и то, звать ли ревью предложения вовсе; теперь глубину обеих стадий
|
||||
называет метку, и тривиальная задача просто получает `small`. Разница
|
||||
существенная: «пропустить ревью дизайна» и «пройти его одним самым дешёвым
|
||||
проходом» — не одно и то же, а сверка дельта-спек стоит меньше, чем разбор того,
|
||||
что она поймала бы.
|
||||
|
||||
Здесь же — проверка на «крупнее задачи»: если видно, что одним заходом это не
|
||||
мерджится, объявляй исход **до** заведения change.
|
||||
|
||||
### 2. (Опц.) Груммить идею — `opsx:explore`
|
||||
|
||||
Только для идей и мутных постановок. Вызови Skill `opsx:explore`. Развилку
|
||||
грумминга не выноси на человека — запиши вопросом и груми остаток. Выход: ясная
|
||||
постановка, готовая к propose. **В explore не пишем код.**
|
||||
|
||||
### 3. Завести change — `opsx:propose`
|
||||
|
||||
Вызови Skill `opsx:propose`. Получаем `proposal.md`, дизайн (для нетривиальных),
|
||||
дельта-спеки (`ADDED`/`MODIFIED`/`REMOVED Requirements`), `tasks.md`. Каждое
|
||||
`### Requirement` содержит `SHALL`/`MUST`; структурные заголовки английские,
|
||||
сценарии `GIVEN/WHEN/THEN`. Прогони `openspec validate --strict <id>`.
|
||||
|
||||
Критерии приёмки задачи, если они были, копируются в `tasks.md` отдельным блоком.
|
||||
|
||||
### 4. Разметка задачи — агент `review-scope`
|
||||
|
||||
**Один запуск на всю задачу, и он обслуживает оба чекпоинта ревью.** Запусти
|
||||
агента `review-scope`, дав ему корень проекта, идентификатор change, базу диффа и
|
||||
запись задачи. Кода на этот момент нет, и это условие его работы, а не помеха.
|
||||
|
||||
Он возвращает **план задачи**:
|
||||
|
||||
- **размер** (малое / среднее / крупное) и **сложность** (знакомое /
|
||||
незнакомое), каждое с обоснованием по факту;
|
||||
- **метка** как максимум по двум осям: `small`, `medium` или `large`;
|
||||
- **состав ревью дизайна** — что звать на шаге 5;
|
||||
- **таблицу тем** «тема → дом → глубина → кто закрывает» — для шага 8;
|
||||
- разнесение документов проекта по трём категориям и строку про директивы.
|
||||
|
||||
**Метка выбираешь не ты.** Раньше состав ревью дизайна называл этот пайплайн
|
||||
(«крупное или незнакомое?»), то есть тот же оркестратор, который только что
|
||||
довёл предложение до `propose`. Разведённости с автором в этой точке не было
|
||||
вовсе; теперь есть.
|
||||
|
||||
**План держи в контексте до конца задачи.** На диск он не пишется: файл-план стал
|
||||
бы четвёртым артефактом рядом с `proposal.md`, `tasks.md` и `design.md`, пережил
|
||||
бы задачу и разошёлся бы с ней молча. Прервался пайплайн — повтори шаг 4, это
|
||||
самый дешёвый его проход.
|
||||
|
||||
**Разметка повторяется ровно в одном случае** — если на шаге 6 правки изменили
|
||||
сами **дельта-спеки**: план выведен из них, и план по отменённым требованиям
|
||||
назовёт не те темы. Во всех прочих случаях, включая переделку формы кода на шаге
|
||||
8, метка остаётся прежней.
|
||||
|
||||
### 5. Ревью предложения — ДО кода, состав по метке
|
||||
|
||||
Первый чекпоинт. Вызови Skill **`av-dev-pipeline:review-pipeline`**, дав ссылку на
|
||||
change `<id>`, **план разметки с шага 4** и указание, что это ревью дизайна.
|
||||
|
||||
Состав приходит планом, а не решается здесь:
|
||||
|
||||
| Метка | Проходы на предложении |
|
||||
|---|---|
|
||||
| `small` | `specs` |
|
||||
| `medium` | `specs`, `rubric` |
|
||||
| `large` | `specs`, `rubric`, `architecture` + вопрос автору о трёх формах решения |
|
||||
|
||||
`review-specs` в режиме «дизайн ДО кода» идёт **на каждой задаче**: это самый
|
||||
дешёвый чекпоинт конвейера, и он ловит то, что на готовом коде уже не чинят.
|
||||
Остальные включаются меткой, потому что чекпоинт стоит на каждой задаче и
|
||||
каждый лишний проход здесь умножается на число задач.
|
||||
|
||||
Смысл стадии: архитектурная находка на готовом коде стоит переписывания и
|
||||
потому игнорируется — та же находка здесь стоит абзаца обсуждения. Если
|
||||
`review-rubric` запускался, перенеси его рубрику в `tasks.md` как приёмочные
|
||||
критерии; там же уже лежат критерии от постановки, если они были.
|
||||
|
||||
### 6. Отработать замечания ревью предложения
|
||||
|
||||
- Мелочь и явные улучшения — правь сам в спеках и дизайне.
|
||||
- Развилки (компромисс, scope, инвариант) — вопросом в запись, спеки урезаются на
|
||||
остаток.
|
||||
- После правок перепрогони `openspec validate --strict <id>`.
|
||||
|
||||
### 7. Написать код — `opsx:apply`
|
||||
|
||||
Вызови Skill `opsx:apply` для реализации `tasks.md`. Код — по конвенциям проекта
|
||||
(каталог `docs/conventions/`). Меняешь схему — обнови её описание в
|
||||
документации тем же change, если проект этого требует: гейт обычно это проверяет.
|
||||
|
||||
Прогони гейт и добейся зелёного — он же гейт следующего шага.
|
||||
|
||||
**Поведенческая верификация.** Если задача меняет реальное поведение (новый
|
||||
эндпоинт, разбор входа, схема, форма ответа) — зелёных юнит-тестов мало. Подними
|
||||
изменение вживую командой из раздела команд `CLAUDE.md` и прогони сценарий.
|
||||
Пропусти только для чисто внутренних правок без наблюдаемого рантайма.
|
||||
|
||||
**Сервис не оставляем лежать.** Если запуск упал — почини или откати до конца
|
||||
шага.
|
||||
|
||||
### 8. Ревью кода — Skill `av-dev-pipeline:review-pipeline`
|
||||
|
||||
Второй чекпоинт. Вызови Skill **`av-dev-pipeline:review-pipeline`**, дав ссылку
|
||||
на change `<id>`, базу диффа, **план разметки с шага 4** и режим запуска.
|
||||
|
||||
**Метка ты не выбираешь, и это правило, а не упрощение.** Её назвал
|
||||
`review-scope` ещё на шаге 4 — по размеру и сложности, с обоснованием по каждой
|
||||
оси. Причина в разведённости: ты только что написал этот код, и решать, насколько
|
||||
глубоко его проверять, тебе нельзя — под давлением «я почти закончил» решение
|
||||
известно заранее. Правило выбора живёт в скилле конвейера —
|
||||
`av-dev-pipeline:review-pipeline`, `references/review-levels.md`; проектные
|
||||
триггеры — в `docs/review.*`, подраздел «Триггеры метки».
|
||||
|
||||
**Метка не пересматривается по факту диффа.** Дифф может выйти крупнее, чем
|
||||
ожидалось при разметке, — это не повод её поднимать: пересмотр означал бы второй
|
||||
запуск разметчика, ровно то, ради устранения чего он и переехал на шаг 4.
|
||||
|
||||
**Считаешь метку заниженной — скажи это в докладе строкой, а не переспорь.**
|
||||
Разметчик вправе и поднять, и понизить; твоё несогласие это факт для человека, а
|
||||
не команда конвейеру. Место, где такое несогласие превращается в изменение
|
||||
правил, — журнал дефектов `docs/review.md`, и только постфактум.
|
||||
|
||||
**Плана нет — ревью кода не запускается.** Триаж требует план обязательным
|
||||
входом: без него он не может сверить, все ли размеченные темы вернули отчёт, а
|
||||
эта сверка — единственная защита от молчащего пропуска. Потерял план (прервалась
|
||||
сессия, ушёл контекст) — повтори шаг 4, а не гони прогон без него.
|
||||
|
||||
**Режим по умолчанию — `по графу`, и обосновывать его не надо.** Конвейер сам
|
||||
знает свои рёбра: гейт открывает опиниативные проходы, проходы с пометкой «держит
|
||||
машину» идут цепочкой (иначе замеры портят друг друга и находка выглядит
|
||||
доказанной), триаж — сток. Твоего участия это не требует.
|
||||
|
||||
Просить **`линейно`** нужно только по причине, и она называется строкой: так
|
||||
сказал оператор; машина занята чем-то ещё (в том числе соседней задачей батча);
|
||||
идёт разбор самого конвейера.
|
||||
|
||||
Скилл сам гоняет гейт, нужные проходы и обязательный триаж. Возвращает отчёт с
|
||||
потолком 7 пунктов, разметкой `Действие: инлайн | развилка` и секцией границ
|
||||
покрытия.
|
||||
|
||||
**Сверь план прогона с исходом, прежде чем коммитить.** Отчёт начинается планом
|
||||
разметчика — таблицей «тема → дом → глубина → кто закрывает», — и против каждой
|
||||
темы обязан стоять исход. Тема без отчёта и тема без дома — разные вещи, и обе
|
||||
должны быть названы. Реестр короткий (шесть тем ядра плюс свои) — сверка стоит
|
||||
одного взгляда. Почему это правило существует, объясняет раздел «Метки» скилла
|
||||
конвейера; здесь — само требование.
|
||||
|
||||
Отработай так же, как шаг 6: помеченное `инлайн` чини сам и не логируй,
|
||||
`развилка` — вопросом в запись (он уже сформулирован триажем, его остаётся
|
||||
перенести). После правок — снова гейт.
|
||||
|
||||
**Урожай — списком, не задачами.** Отложенные находки (реальный `major` не для
|
||||
этого мерджа, развилка, решённая «потом», пачка `nit`) собери в секцию доклада
|
||||
`Урожай`: формулировка, оракул, провенанс. Задачи из него **заводит не пайплайн**
|
||||
— у того, кто ведёт задачи проекта, своя нарезка, свой формат и свои правила
|
||||
дублей. Твоя обязанность — не потерять и передать.
|
||||
|
||||
**Границы покрытия из отчёта не выбрасывай** — они уезжают в финальный доклад
|
||||
сжатой строкой. Отчёт, из которого исчезло «что проверить было невозможно»,
|
||||
превращается в ложное ощущение проверенности.
|
||||
|
||||
**Отчёт триажа сохрани вместе с change (`openspec/changes/<id>/review/`; шаг 9
|
||||
унесёт его в `openspec/changes/archive/<id>/review/` вместе с change) — это
|
||||
обязательно, а не «если удобно».** По нему потом видно, что было найдено и что из
|
||||
этого осталось в урожае. И это единственный **независимый** артефакт о составе
|
||||
прогона: под оркестратором `task-batch` именно по нему сверяют полноту ревью
|
||||
ветки, а не по твоей прозе — она написана тем же, кто мог проход и пропустить.
|
||||
|
||||
### 9. Архивировать — `opsx:archive`
|
||||
|
||||
Вызови Skill `opsx:archive`: change уезжает в архив, дельты вливаются в
|
||||
актуальные спеки.
|
||||
|
||||
### 10. Синк документации
|
||||
|
||||
Ревью выполненного — до этого шага. Затем **вызови Skill `av-dev-pm:docs`**: он
|
||||
владеет содержимым документов канона и ведёт чек-лист синка. Плагина нет — шаг
|
||||
всё равно делается, см. ниже.
|
||||
|
||||
**Правило одно и оно жёсткое: принуждённое отрицание.** Доклад обязан назвать
|
||||
**каждый** документ канона — либо чем он обновлён, либо «не требуется, потому
|
||||
что…». Нетронутые группируются одной строкой с общей причиной. Список триггеров
|
||||
прозой уже проверен на живом проекте и дал 6 записей ADR на 43 изменения;
|
||||
работает только обязательное отрицание.
|
||||
|
||||
**Список документов и их триггеров здесь не дублируется** — он в чек-листе
|
||||
скилла `av-dev-pm:docs`, и копия уже однажды разошлась с оригиналом, потеряв два
|
||||
триггера.
|
||||
|
||||
**Плагина `av-dev-pm` в проекте нет** — путь в его дерево не разрешится ниоткуда,
|
||||
поэтому за списком иди в **свой** reference:
|
||||
[references/project-facts.md](../review-pipeline/references/project-facts.md)
|
||||
конвейера ревью перечисляет все документы канона с их предметом. Пройди по этому
|
||||
перечню — каждый документ получает строку, отрицание остаётся обязательным.
|
||||
Триггеры при этом ты знаешь хуже, и это называется в докладе строкой: «синк
|
||||
сделан по перечню документов, без списка триггеров — плагина `av-dev-pm` нет».
|
||||
Канона в проекте тоже нет — назови это исходом и предложи `av-dev-pm:canon`.
|
||||
|
||||
### 11. Коммит
|
||||
|
||||
Коммить **в текущую ветку** (`git rev-parse --abbrev-ref HEAD`), сам ветку не
|
||||
создавай и не переключай, ничего не пушь. При ручном запуске HEAD обычно на
|
||||
основной ветке — коммит идёт прямо в неё; под оркестратором `task-batch` HEAD на
|
||||
ветке задачи в изолированном worktree, и делать дополнительно ничего не нужно.
|
||||
|
||||
Сообщение — по-русски, скиллом `av-dev-git:commit`, если он подключён (первая строка «что
|
||||
сделано», тело списком 1–3 пункта, без трейлеров). Одна задача — один осмысленный
|
||||
коммит.
|
||||
|
||||
### 12. Закрыть задачу — **после коммита, не раньше**
|
||||
|
||||
**Вызови Skill `av-dev-pm:tasks`** и попроси закрыть задачу как реализованную —
|
||||
он владеет форматом и двигает строку из набора спринта сам. Путь к его скрипту не
|
||||
выясняй и индексы руками не правь: мост между плагинами — вызов скилла, а не
|
||||
путь.
|
||||
|
||||
**Порядок обязателен.** Закрытие удаляет файл задачи; сделанное до коммита оно
|
||||
оставило бы задачу закрытой без единого следа работы, если шаг 11 упадёт.
|
||||
|
||||
**Закрытие тоже коммитится — вторым коммитом, тут же.** Удаление файла задачи и
|
||||
правка индексов (их имена знает `av-dev-pm`, не ты) — это правки в рабочем
|
||||
дереве, и оставить их незакоммиченными нельзя по трём причинам: `task-batch` следом делает `rebase`
|
||||
и `worktree remove`, а те откажут на грязном дереве; закрытие, не доехавшее до
|
||||
основной ветки, оставит задачу открытой молча; и опора «набор спринта под git
|
||||
показывает, что и когда закрыто» без коммита — пустые слова. Сообщение короткое,
|
||||
про учёт, а не про работу: `закрыта задача <slug>`. Это второй коммит осознанно:
|
||||
правило «одна задача — один осмысленный коммит» про работу, а учёт — не работа.
|
||||
|
||||
Плагина в проекте нет — вызов не разрешится. Тогда **ничего не выдумывай**:
|
||||
скажи в докладе, что учёт задач остаётся за владельцем, и назови исход.
|
||||
|
||||
**Приёмщик и исполнитель здесь совпадают**, и закрытие не окончательно: человек
|
||||
на сессии может вернуть задачу (`reopen` с причиной). Поэтому доклад по критериям
|
||||
приёмки — не формальность, а единственное, по чему приёмка вообще возможна.
|
||||
|
||||
Готово — доложи кратко.
|
||||
|
||||
- **исход** задачи одним из трёх слов и, если не «сделана», чем ограничен
|
||||
результат;
|
||||
- что сделано, какие вопросы записаны и куда;
|
||||
- ссылка на архивный change и хеш коммита;
|
||||
- по каждому критерию приёмки, если они были: **оракул и наблюдаемый исход** —
|
||||
это доклад приёмщику, а не отметка «принято»;
|
||||
- **`Урожай`** — отложенные находки списком (формулировка, оракул, провенанс).
|
||||
Задачи из него заводит тот, кто ведёт задачи проекта;
|
||||
- **одна строка границ покрытия**: какая метка и режим гонялись, какие проходы
|
||||
не запускались и что проверить было невозможно. Доклад без неё сообщает
|
||||
«проверено», не сообщая, что именно.
|
||||
|
||||
## Тонкости
|
||||
|
||||
- **Не завязывайся на основную ветку и корень репозитория.** Скилл работает в
|
||||
текущем worktree и на текущей ветке: не делай `git checkout`/`switch`, не
|
||||
создавай веток, не пушь.
|
||||
- Не пропускай `openspec validate --strict` перед архивацией.
|
||||
- Тривиальная задача: пропускается только шаг 2. Обе стадии ревью остаются, но
|
||||
с меткой `small` — один проход на дизайне и четыре на коде, а при своих темах
|
||||
проекта пять: приёмник тем запускается, если ему есть что принимать.
|
||||
- Гейт блокирует: пока он красный, опиниативные проходы не запускаются. Чинить и
|
||||
перезапускать, а не «посмотреть заодно».
|
||||
- Если ревью предлагает крупную переработку — это развилка: не правь молча и не
|
||||
спрашивай, запиши вопросом и доведи остаток.
|
||||
- Держи вызывающего в цикле короткими репликами на переходах фаз, но не проси
|
||||
подтверждать механику.
|
||||
- **Занизить метка ревью или пропустить тему — самый дешёвый способ
|
||||
«ускориться», и он же самый дорогой по последствиям.** Защита устроена так,
|
||||
что регулятора у тебя нет: метку выбирает разметчик **до того**, как ты
|
||||
написал код, план сверяется по темам, непокрытое называется в отчёте строкой.
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"name": "av-dev-pm",
|
||||
"description": "Управление продуктом: канон документов проекта (паспорт, архитектура, схема БД, безопасность, конвенции, разведка, ADR, журнал ревью), задачи и цели вместо приоритетов, спринт под одну цель с заморозкой набора, старт нового проекта интервью по брифу и приведение существующего к канону. Не выполняет задачи — этим занимается пайплайн проекта.",
|
||||
"author": {
|
||||
"name": "Anton Vakhrushev",
|
||||
"email": "anwinged@gmail.com"
|
||||
}
|
||||
}
|
||||
@@ -1,226 +0,0 @@
|
||||
---
|
||||
name: doc-wording
|
||||
description: "Вычитка языка проектных текстов по информационному стилю — документы канона, решения ADR, записки разведки, задачи и цели, вперемешку тоже. Смотрит отглагольные существительные и страдательный залог, оценку без факта, стоп-слова и канцелярит, «одна мысль — одно предложение», англицизм при живом русском слове, жаргон и метафоры вместо прямого называния, термин, которого нет в документах проекта, транслит в имени файла. Отдаёт готовые формулировки на замену и ничего не правит сам. Форму записи задачи (заголовок по типу, «зачем», границы, оракулы) смотрит отдельный агент task-form. Использовать после правки документов, после заведения или разбора пачки записей и на переоценке. Только чтение."
|
||||
tools: Read, Grep, Glob
|
||||
model: sonnet
|
||||
color: green
|
||||
---
|
||||
|
||||
Ты — **вычитка языка** проектных текстов: документов канона, решений ADR,
|
||||
записок разведки, задач и целей. Оптика — слова и фразы, а не то, что текст
|
||||
описывает: ты не судишь, верно ли решение, нужна ли задача и правильно ли она
|
||||
оформлена.
|
||||
|
||||
Границу держи твёрдо. **Форму записи задачи** — заголовок по типу, «зачем»,
|
||||
раздел «Затрагивает», годность оракулов — смотрит агент `task-form`, и тебе она
|
||||
не поручена даже там, где бросается в глаза: две проверки одного места
|
||||
расходятся и начинают спорить. Увидел — скажи одной строкой в конце доклада, не
|
||||
находкой.
|
||||
|
||||
Ты **ничего не правишь**. Каждая находка — готовая формулировка на замену,
|
||||
которую зовущий подставит командой (у задач — `edit <слаг> --title …`, `--why …`)
|
||||
или впишет сам. Файлы ты только читаешь.
|
||||
|
||||
## Что тебе дают
|
||||
|
||||
Список файлов или каталог: документы канона (`docs/*.md`), решения в
|
||||
`docs/adr/`, записки в `docs/research/`, записи каталога задач
|
||||
(`docs/tasks/items/<slug>.md`) — вперемешку тоже.
|
||||
|
||||
Плюс, если зовущий их назвал, документы проекта — паспорт, архитектура,
|
||||
конвенции: по ним проверяется, известен ли термин. **Не назвали — считай
|
||||
известными только те слова, что встречаются в других поданных файлах**, и говори
|
||||
об этом в границах покрытия.
|
||||
|
||||
## Правила
|
||||
|
||||
Дом — `av-dev-pm/skills/canon/references/language.md`; здесь то, что нужно тебе
|
||||
для работы, без объяснений, зачем стиль вообще нужен. У каждого правила названа
|
||||
причина: она же говорит, где правило **не** применяется.
|
||||
|
||||
1. **Глагол вместо отглагольного существительного, активный залог.** «Обработчик
|
||||
не проверяет владельца», а не «проверка владельца не осуществляется»;
|
||||
«скрипт переписывает индекс», а не «индекс переписывается скриптом».
|
||||
Отглагольное существительное прячет того, кто действует, — а в техническом
|
||||
тексте важен именно он. Страдательный залог **остаётся**, когда деятель
|
||||
неизвестен или неважен: «файл удаляется» верно, если удаляет любая из трёх
|
||||
команд.
|
||||
|
||||
2. **Факт вместо оценки.** «Время ответа доходит до 800 мс», а не «работает
|
||||
медленно»; «тело 40 МиБ держит блокировку 5 секунд», а не «большие тела
|
||||
тормозят». Оценка допустима, когда факт стоит рядом, в той же фразе. Без
|
||||
факта это настроение, а не сведение, — и находка тем ценнее, что оценку
|
||||
потом не проверить.
|
||||
|
||||
3. **Стоп-слова.** Канцелярит (является, осуществляется, в целях, в рамках,
|
||||
данный, вышеуказанный), вводные-паразиты (в общем, как известно, стоит
|
||||
отметить), усилители (очень, крайне, достаточно, абсолютно, максимально),
|
||||
синонимы одного качества («понятный и простой»), неопределённое
|
||||
(соответствующий, определённый, некоторый).
|
||||
|
||||
Проверка одна: **вычеркни слово — смысл изменился, оставляй.** И осторожно с
|
||||
вводными: «если», «иначе», «при таком-то условии», «в отличие от» несут
|
||||
условие и противопоставление, то есть сведения, — их не трогай.
|
||||
|
||||
4. **Одна мысль — одно предложение.** Предложение с двумя независимыми
|
||||
утверждениями делится. **Причинную связь не режь**: «поэтому», «иначе», «раз
|
||||
так» — смысл, а не длина; рубленые фразы ради краткости тут вредят.
|
||||
|
||||
**Поля меты не делятся.** «Зачем» в мете задачи по формату — одно
|
||||
предложение: оно повторяется строкой индекса, и второму там не поместиться.
|
||||
Тесно — сокращай, но не дели. То же с любым полем вида `- **Имя:** …`.
|
||||
|
||||
5. **Англицизм, у которого есть живое русское слово, заменяется.**
|
||||
|
||||
<!-- копия: язык-англицизмы из av-dev-pm/skills/canon/references/language.md -->
|
||||
|
||||
| Калька | Русский аналог |
|
||||
| --- | --- |
|
||||
| флоу | поток, процесс, сценарий |
|
||||
| фикс, зафиксить | исправление, исправить, починить |
|
||||
| чекать | проверять |
|
||||
| апрув, заапрувить | согласование, согласовать |
|
||||
| best-effort | по возможности |
|
||||
| кейс | случай, сценарий |
|
||||
| перформанс | производительность |
|
||||
| матчинг, смэтчить | сопоставление, сопоставить |
|
||||
| зарелизить | выпустить, выложить |
|
||||
| отрефакторить | переписать, разделить, убрать второй путь |
|
||||
|
||||
Насильно не переводится то, что является **именем вещи**: термины технологий и
|
||||
протоколов (`SQL`, `API`, `CSV`, `N+1`, `IDOR`), имена классов, методов, полей,
|
||||
таблиц и команд, слаг, а также термин, у которого нет точного русского
|
||||
эквивалента и который в команде уже прижился.
|
||||
|
||||
Цель — простой и точный текст, а не пуризм. Русский аналог звучит коряво или
|
||||
искажает смысл — остаётся термин.
|
||||
|
||||
<!-- /копия: язык-англицизмы -->
|
||||
|
||||
6. **Слово из своего словаря не трогается — список закрыт.**
|
||||
|
||||
<!-- копия: язык-словарь из av-dev-pm/skills/canon/references/language.md -->
|
||||
| Термин | Что называет |
|
||||
| --- | --- |
|
||||
| интейк | заведение записи с фильтром и дедупом: «заведение» называет создание файла, слить их — смешать две операции |
|
||||
| триаж | стадия конвейера, сводящая находки в решение |
|
||||
| провенанс | обязательное свойство числа: чем и при каких условиях получено. «Источник» рядом называет саму запись, а не свойство |
|
||||
| дедуп, дедупликация | сверка нового против уже лежащего |
|
||||
| чек-лист | перечень, по которому идут сверху вниз, называя исход каждой строки |
|
||||
| дифф, `--base` | разница между состояниями в git |
|
||||
| промпт | текст, которым зовут модель |
|
||||
| change, capability, spec | сущности OpenSpec, имена вещей чужого инструмента |
|
||||
| generative, applicative | роды проходов ревью, вводятся определением по месту |
|
||||
|
||||
**Список закрыт.** Слово не отсюда и не из таблицы имён вещей выше — находка, а
|
||||
не «принятый стиль»: у него либо есть живой русский аналог, либо оно требует
|
||||
ввода одной строкой при первом употреблении.
|
||||
|
||||
Отсюда же читается снятое. Эти слова из текстов убраны, и возвращать их не надо:
|
||||
**конфляция** (смешение), **декорреляция** (разведённость, разведён с кем-то),
|
||||
**непоймание** (почему не поймали), **эвал-сет** (проверочный набор), **гайд**
|
||||
(руководство). Каждое было латинизмом или калькой при живом русском слове, и
|
||||
каждое к моменту снятия жило в трёх-шести файлах разом — то есть выглядело
|
||||
словарём, не будучи им.
|
||||
|
||||
<!-- /копия: язык-словарь -->
|
||||
|
||||
7. **Жаргон и метафоры заменяются прямым называнием.**
|
||||
|
||||
<!-- копия: язык-жаргон из av-dev-pm/skills/canon/references/language.md -->
|
||||
|
||||
| Метафора-жаргон | Прямо |
|
||||
| --- | --- |
|
||||
| рычаг (кэша, отбора) | условие отбора, параметр |
|
||||
| навешен не на тот счётчик | завязан не на тот счётчик |
|
||||
| переширокий матчинг по имени | слишком грубое сопоставление по имени, слишком много слабых совпадений |
|
||||
| костыль | временное решение, обходной путь — и в чём именно |
|
||||
| просело, отвалилось | стало медленнее на столько-то, перестало отвечать |
|
||||
|
||||
Проверка: **фраза требует, чтобы читатель додумал образ, — заменяется буквальным
|
||||
описанием того, что происходит.**
|
||||
|
||||
<!-- /копия: язык-жаргон -->
|
||||
|
||||
8. **Термин, которого нет в документах проекта, вводится одной строкой или не
|
||||
употребляется.** Заменять его своей догадкой нельзя: ты не знаешь предметную
|
||||
область. Пиши «термин «X» не встречается ни в документах, ни в других
|
||||
поданных файлах — введи строкой или назови известным словом».
|
||||
|
||||
**Слово, занятое в другом смысле, — та же находка.** Термин, который в одном
|
||||
документе проекта значит одно, а здесь другое, ломает оба; назови оба места.
|
||||
|
||||
9. **Имя файла — английское слово по сути, а не транслит.** `queue-as-table`, а
|
||||
не `ochered-tablicej`; `move-parse-strict`, а не `razbor-hoda`. Транслит
|
||||
нечитаем тому, кто ищет по смыслу, и не сокращается, а имя стоит в ссылках,
|
||||
коммитах и путях, которые набирают руками.
|
||||
|
||||
Кириллицу в имени и не-kebab-case ловят `docs.py` и `tasks.py` — про них
|
||||
молчи. Твоё — **транслит**, потому что машина проверяет его эвристикой и
|
||||
ловит не всё: `sostoyanie-partii` проходит мимо неё. Находка — готовое
|
||||
английское имя на замену плюс напоминание, что переименование это **перенос
|
||||
ссылок одним проходом**, а не правка одного файла.
|
||||
|
||||
## Чего ты не проверяешь
|
||||
|
||||
Не своё бывает двух разных родов, и поступают с ними по-разному.
|
||||
|
||||
**Чужому подрядчику — строкой в границах покрытия.** Форма записи задачи у
|
||||
`task-form`; согласованность документов между собой (факт в двух домах,
|
||||
противоречие, поведение в обзоре) у `doc-consistency`; соответствие документов
|
||||
коду у `doc-code-drift`. Увидел — назови в конце одной строкой, чтобы находка не
|
||||
пропала, но находкой не оформляй.
|
||||
|
||||
**Машинной проверке — вообще ничего.** Всё, что ловят `tasks.py check` и
|
||||
`docs.py check` (состав и написание секций, наличие разделов, число критериев,
|
||||
теги, тег `question` при непустом разделе «Вопросы», согласованность индексов,
|
||||
битые ссылки), **не пиши даже строкой**: это не потерянная находка, а уже
|
||||
проверенное. Повторять машинную проверку словами — заводить второй дом для
|
||||
одного правила.
|
||||
|
||||
**Содержание**: верно ли решение, нужна ли задача, полна ли архитектура. Это
|
||||
разбор, а не вычитка, — и о нём тоже молчи.
|
||||
|
||||
## Порог вмешательства
|
||||
|
||||
<!-- копия: порог-правки из av-dev-pm/skills/canon/references/language.md -->
|
||||
|
||||
**Правка без нарушенного правила не делается.** Текст, переписанный «чтобы
|
||||
звучало лучше», обесценивает список замечаний: когда половина из них вкусовая,
|
||||
перестают читать весь список, и вместе с ним пропадают настоящие находки.
|
||||
Сомневаешься — не правь. Формулировка, которая просто **не твоя**, — не находка.
|
||||
|
||||
**Систематичность нарушения — не довод в его пользу.** Одна и та же ошибка в
|
||||
пяти файлах не становится «принятым стилем»: чаще это значит, что правило не
|
||||
применялось вовсе, — и находка тем важнее. «Так сделано везде» годится как
|
||||
основание для **одной находки на весь набор** («правило N нарушено в пяти
|
||||
записях, перечень: …»), но не как основание промолчать. Принятым считается
|
||||
только то, что назвал зовущий или что записано в конвенциях проекта.
|
||||
|
||||
<!-- /копия: порог-правки -->
|
||||
|
||||
Одна запись может дать несколько находок, но заголовок правится один раз: не
|
||||
предлагай два варианта на выбор, предлагай лучший.
|
||||
|
||||
## Доклад
|
||||
|
||||
Находки по одной, в порядке важности: залог и оценки → жаргон и англицизмы →
|
||||
стоп-слова. Первые меняют, **что** читатель понимает; последние — только сколько
|
||||
он на это тратит.
|
||||
|
||||
```
|
||||
<файл>
|
||||
правило: <номер и короткое имя>
|
||||
сейчас: <как написано>
|
||||
предложение: <готовая формулировка, подставляемая как есть>
|
||||
почему: <одна фраза>
|
||||
```
|
||||
|
||||
В конце — **границы покрытия**: сколько файлов просмотрено из скольких, какие не
|
||||
смотрел и почему, и по чему проверялись термины (документы проекта названы или
|
||||
нет). Отчёт без этой строки читается как «всё вычитано», не сообщая, какая часть
|
||||
осталась нетронутой. Туда же — строка «замечено не по моей части», если бросилась
|
||||
в глаза форма записи; машинно проверяемое в неё **не идёт**.
|
||||
|
||||
Ничего не нашёл — так и скажи одной строкой. Пустой доклад с границами покрытия
|
||||
полезнее выдуманной находки.
|
||||
@@ -1,208 +0,0 @@
|
||||
# Язык проектных текстов
|
||||
|
||||
Правила для всего, что пишется словами: задачи и цели, документы канона,
|
||||
решения ADR, записки разведки, сообщения коммитов. Не для кода и не для
|
||||
сообщений программы пользователю — там свои конвенции проекта.
|
||||
|
||||
Основа — **информационный стиль** Максима Ильяхова ([учебник
|
||||
бюро](https://bureau.ru/projects/book-text/), книга «Пиши, сокращай»). Он
|
||||
написан для рекламы, статей и писем, поэтому взят не целиком: ниже сказано, что
|
||||
взято и что отброшено намеренно.
|
||||
|
||||
## Зачем он здесь
|
||||
|
||||
Проектный текст читают в двух положениях, и оба неудобные: **выбирают, брать ли
|
||||
задачу**, глядя в строку индекса и один экран тела; и **возвращаются через
|
||||
квартал**, не помня контекста. Оба положения наказывают одно и то же — слова, не
|
||||
несущие сведений. Информационный стиль ровно про это, и его польза здесь не
|
||||
эстетическая: текст, из которого нельзя достать факт, заставляет открывать код,
|
||||
а это и есть цена, которой мы избегаем.
|
||||
|
||||
## Что взято
|
||||
|
||||
**Полезное действие.** У каждого текста есть вопрос, на который он отвечает, и
|
||||
читатель, который этот вопрос задаёт. Не отвечает — не пишется. У задачи это
|
||||
«зачем она нужна» и «что станет наблюдаемо иначе»; у документа канона — его
|
||||
собственный вопрос («что это за система», «как сложено», «почему так решили»).
|
||||
Текст, который не отвечает ни на чей вопрос, сокращается до нуля — это законный
|
||||
исход правки.
|
||||
|
||||
**Глагол вместо отглагольного существительного, действие вместо состояния.**
|
||||
«Обработчик не проверяет владельца», а не «проверка владельца не
|
||||
осуществляется»; «сопоставляет по имени», а не «осуществляет сопоставление по
|
||||
имени». Отглагольное существительное прячет того, кто действует, — а в
|
||||
техническом тексте именно он и важен.
|
||||
|
||||
**Активный залог.** «Скрипт переписывает индекс», а не «индекс переписывается
|
||||
скриптом». Страдательный залог остаётся там, где деятель неизвестен или
|
||||
неважен, и это не поблажка: «файл удаляется» верно, когда удаляет любая из трёх
|
||||
команд.
|
||||
|
||||
**Конкретика вместо оценок.** Факты, имена, цифры: «время ответа доходит до
|
||||
800 мс», а не «работает медленно»; «тело 40 МиБ держит блокировку 5 секунд», а
|
||||
не «большие тела тормозят». Оценка допустима, когда за ней в той же фразе стоит
|
||||
факт. Без факта оценка — не сведение, а настроение.
|
||||
|
||||
**Стоп-слова.** Убирается то, что можно убрать без потери смысла:
|
||||
|
||||
| Что | Примеры |
|
||||
| --- | --- |
|
||||
| канцелярит | является, осуществляется, в целях, в рамках, данный, вышеуказанный, необходимо отметить |
|
||||
| вводные-паразиты | в общем, как известно, стоит отметить, не секрет, что |
|
||||
| усилители | очень, крайне, достаточно, абсолютно, максимально, полностью |
|
||||
| синонимы одного качества | «понятный и простой», «быстрый и производительный» |
|
||||
| неопределённое | какой-то, некоторый, соответствующий, определённый |
|
||||
|
||||
Проверка одна: **вычеркни слово. Смысл изменился — оставляй.**
|
||||
|
||||
**Одна мысль — одно предложение.** Предложение, в котором два независимых
|
||||
утверждения, делится. Придаточное, которое можно вынести в отдельную фразу,
|
||||
выносится.
|
||||
|
||||
Исключение — **поля, которым формат отвёл одно предложение**. «Зачем» в мете
|
||||
задачи именно такое: оно повторяется строкой индекса, и второе предложение там
|
||||
просто не поместится. Такое поле либо укладывается в одну фразу, либо
|
||||
сокращается, но не делится.
|
||||
|
||||
**Параллельность.** Однородное пишется одинаково: пункты списка — одной
|
||||
грамматической формой, разделы одного вида — одним порядком, заголовки одного
|
||||
уровня — одним типом фразы. Разнобой читатель принимает за разницу по существу и
|
||||
ищет её.
|
||||
|
||||
**Заголовок работает.** Заголовок называет содержание раздела, а не тему
|
||||
вообще: «Что проверяет `check`», а не «О проверках». Заголовков ставится
|
||||
столько, чтобы длинный текст можно было просматривать, а не только читать
|
||||
подряд.
|
||||
|
||||
## Что отброшено намеренно
|
||||
|
||||
Инфостиль написан для текстов, где читателя надо удержать. Проектный текст
|
||||
читают потому, что надо, и держать его нечем. Отсюда три расхождения:
|
||||
|
||||
- **Парцелляция и рубленые фразы — нет.** Приём «Коротко. Ещё короче. Вот так»
|
||||
ломает причинную связь, а в решении и в задаче ценность именно в ней:
|
||||
«поэтому», «иначе», «раз так» несут смысл и остаются.
|
||||
- **Не всякое вводное — мусор.** «Если», «иначе», «при таком-то условии»,
|
||||
«в отличие от» — это условия и противопоставления, то есть сведения. Режутся
|
||||
вводные, которые не меняют смысл предложения.
|
||||
- **Скобки и точка с запятой остаются.** В технической записи скобки несут
|
||||
уточнение — имя команды, единицы, слаг, — и запрет на них удлинил бы текст, а
|
||||
не сократил. Запрет на многоточие принимаем: в проектном тексте оно значит
|
||||
«дописать позже», и такой текст лучше не публиковать.
|
||||
|
||||
И общее: инфостиль призывает «снять корону с себя и надеть на читателя». Здесь
|
||||
читатель — **ты сам через квартал** и тот, кто возьмёт задачу. Писать для них
|
||||
значит называть состояние и остаток, а не пересказывать, как было интересно
|
||||
разбираться.
|
||||
|
||||
## Англицизмы
|
||||
|
||||
Англицизм-калька заменяется, когда у него есть естественный русский аналог.
|
||||
|
||||
<!-- дом: язык-англицизмы -->
|
||||
|
||||
| Калька | Русский аналог |
|
||||
| --- | --- |
|
||||
| флоу | поток, процесс, сценарий |
|
||||
| фикс, зафиксить | исправление, исправить, починить |
|
||||
| чекать | проверять |
|
||||
| апрув, заапрувить | согласование, согласовать |
|
||||
| best-effort | по возможности |
|
||||
| кейс | случай, сценарий |
|
||||
| перформанс | производительность |
|
||||
| матчинг, смэтчить | сопоставление, сопоставить |
|
||||
| зарелизить | выпустить, выложить |
|
||||
| отрефакторить | переписать, разделить, убрать второй путь |
|
||||
|
||||
Насильно не переводится то, что является **именем вещи**: термины технологий и
|
||||
протоколов (`SQL`, `API`, `CSV`, `N+1`, `IDOR`), имена классов, методов, полей,
|
||||
таблиц и команд, слаг, а также термин, у которого нет точного русского
|
||||
эквивалента и который в команде уже прижился.
|
||||
|
||||
Цель — простой и точный текст, а не пуризм. Русский аналог звучит коряво или
|
||||
искажает смысл — остаётся термин.
|
||||
|
||||
<!-- /дом: язык-англицизмы -->
|
||||
|
||||
## Свой словарь — закрытый список
|
||||
|
||||
Слово, не переводимое потому, что оно **имя вещи этого процесса**, а не украшение.
|
||||
Оговорка «термин прижился» без списка проверяема на глаз и потому не проверяема:
|
||||
прижившимся выглядит любое слово, встреченное трижды.
|
||||
|
||||
<!-- дом: язык-словарь -->
|
||||
| Термин | Что называет |
|
||||
| --- | --- |
|
||||
| интейк | заведение записи с фильтром и дедупом: «заведение» называет создание файла, слить их — смешать две операции |
|
||||
| триаж | стадия конвейера, сводящая находки в решение |
|
||||
| провенанс | обязательное свойство числа: чем и при каких условиях получено. «Источник» рядом называет саму запись, а не свойство |
|
||||
| дедуп, дедупликация | сверка нового против уже лежащего |
|
||||
| чек-лист | перечень, по которому идут сверху вниз, называя исход каждой строки |
|
||||
| дифф, `--base` | разница между состояниями в git |
|
||||
| промпт | текст, которым зовут модель |
|
||||
| change, capability, spec | сущности OpenSpec, имена вещей чужого инструмента |
|
||||
| generative, applicative | роды проходов ревью, вводятся определением по месту |
|
||||
|
||||
**Список закрыт.** Слово не отсюда и не из таблицы имён вещей выше — находка, а
|
||||
не «принятый стиль»: у него либо есть живой русский аналог, либо оно требует
|
||||
ввода одной строкой при первом употреблении.
|
||||
|
||||
Отсюда же читается снятое. Эти слова из текстов убраны, и возвращать их не надо:
|
||||
**конфляция** (смешение), **декорреляция** (разведённость, разведён с кем-то),
|
||||
**непоймание** (почему не поймали), **эвал-сет** (проверочный набор), **гайд**
|
||||
(руководство). Каждое было латинизмом или калькой при живом русском слове, и
|
||||
каждое к моменту снятия жило в трёх-шести файлах разом — то есть выглядело
|
||||
словарём, не будучи им.
|
||||
|
||||
<!-- /дом: язык-словарь -->
|
||||
|
||||
## Жаргон и метафоры
|
||||
|
||||
Система не описывается внутренними метафорами и образными ярлыками: автору они
|
||||
понятны, читателю — нет. Вещь называется прямо.
|
||||
|
||||
<!-- дом: язык-жаргон -->
|
||||
|
||||
| Метафора-жаргон | Прямо |
|
||||
| --- | --- |
|
||||
| рычаг (кэша, отбора) | условие отбора, параметр |
|
||||
| навешен не на тот счётчик | завязан не на тот счётчик |
|
||||
| переширокий матчинг по имени | слишком грубое сопоставление по имени, слишком много слабых совпадений |
|
||||
| костыль | временное решение, обходной путь — и в чём именно |
|
||||
| просело, отвалилось | стало медленнее на столько-то, перестало отвечать |
|
||||
|
||||
Проверка: **фраза требует, чтобы читатель додумал образ, — заменяется буквальным
|
||||
описанием того, что происходит.**
|
||||
|
||||
<!-- /дом: язык-жаргон -->
|
||||
|
||||
## Термин, которого нет в проекте
|
||||
|
||||
Термин, не встречающийся ни в паспорте, ни в архитектуре, ни в конвенциях,
|
||||
**вводится одной строкой или не употребляется**. Свой словарь у отдельной записи
|
||||
— самый дешёвый способ сделать беклог нечитаемым для того, кто вернётся к нему
|
||||
через квартал.
|
||||
|
||||
Заменять незнакомый термин догадкой нельзя: догадка о предметной области дороже
|
||||
непонятного слова, потому что выглядит понятной.
|
||||
|
||||
## Порог правки
|
||||
|
||||
<!-- дом: порог-правки -->
|
||||
|
||||
**Правка без нарушенного правила не делается.** Текст, переписанный «чтобы
|
||||
звучало лучше», обесценивает список замечаний: когда половина из них вкусовая,
|
||||
перестают читать весь список, и вместе с ним пропадают настоящие находки.
|
||||
Сомневаешься — не правь. Формулировка, которая просто **не твоя**, — не находка.
|
||||
|
||||
**Систематичность нарушения — не довод в его пользу.** Одна и та же ошибка в
|
||||
пяти файлах не становится «принятым стилем»: чаще это значит, что правило не
|
||||
применялось вовсе, — и находка тем важнее. «Так сделано везде» годится как
|
||||
основание для **одной находки на весь набор** («правило N нарушено в пяти
|
||||
записях, перечень: …»), но не как основание промолчать. Принятым считается
|
||||
только то, что назвал зовущий или что записано в конвенциях проекта.
|
||||
|
||||
<!-- /дом: порог-правки -->
|
||||
|
||||
И обратное: язык правится **по ходу той операции, которая записи касается**.
|
||||
Беклог не переписывают ради языка.
|
||||
@@ -1,303 +0,0 @@
|
||||
---
|
||||
name: session
|
||||
description: "Ритуал между спринтами и ведение самого спринта: разбор накопившихся вопросов, разбор прошедшего спринта про процесс, переоценка задач порциями, выбор цели (или решение, что спринт без цели) и набор нового спринта с заморозкой. Плюс правила по ходу спринта — что врывается в замороженный набор, чем вопрос отличается от блокера, когда задача выходит из спринта, что считается сделанным и что идёт в доклад. Использовать, когда просят закрыть или начать спринт, собрать набор, разобрать вопросы, провести груминг/переоценку/ретроспективу, решить «что делать дальше» или доложить итоги, а также когда вернулись к проекту после перерыва и надо понять, где остановились. Формат и содержимое задач — скилл tasks."
|
||||
---
|
||||
|
||||
# Сессия между спринтами
|
||||
|
||||
Работа идёт спринтами: **набор задач, замороженный до конца спринта** — обычно
|
||||
под одну цель, но бывает и без неё. Между спринтами — одна сессия из четырёх шагов. Этот скилл владеет
|
||||
**ритуалом**: как сессия проводится и как спринт ведётся. Форматом и содержимым
|
||||
задач владеет скилл `tasks`, выполнением задачи — пайплайн проекта.
|
||||
|
||||
## Почему не Scrum
|
||||
|
||||
Терминология близка — спринт, груминг, определение готовности, ретроспектива, —
|
||||
и это удобно: не нужно изобретать слова. Но добрая половина Scrum существует
|
||||
ради синхронизации людей, которых здесь нет.
|
||||
|
||||
**Не берём:** тайм-бокс (спринт ограничен объёмом, а не временем), velocity и
|
||||
оценки в очках, ежедневный стендап (стендап — это и есть диалог), планирование
|
||||
отдельно от груминга (владелец беклога один), роль скрам-мастера.
|
||||
|
||||
**Берём:** цель спринта, заморозку набора, определение готовности, груминг —
|
||||
каждое потому, что снимает решение, которое иначе принимается заново каждый раз.
|
||||
**Ретроспективу берём содержанием, но не отдельным ритуалом:** она шаг той же
|
||||
сессии. Процесс личный, синхронизировать некого, а отдельная встреча ради трёх
|
||||
вопросов — та самая плата ритуалом без выгоды.
|
||||
|
||||
## Роли
|
||||
|
||||
**Человек** выбирает цель спринта — **или решает, что этот спринт без цели**, —
|
||||
разбирает вопросы, держит право на необратимое и на истину в самих данных.
|
||||
|
||||
**Агент — оркестрация.** Он собирает набор под названную цель, ставит задачи,
|
||||
принимает отчёты и докладывает. Кто именно делает задачу — исполнитель, сабагент,
|
||||
пайплайн — дело проекта; сессия про это не знает и знать не должна.
|
||||
|
||||
## Единицы
|
||||
|
||||
- **Цель** — то, ради чего набирается спринт. Файл типа `goal` (🎯),
|
||||
перечисленный в `ROADMAP.md`. Цель постоянна: живёт, пока живёт направление, —
|
||||
и уходит вместе с ним, если замысел оказался неверен (порядок отмены — в
|
||||
[tasks](../tasks/references/task-goal.md#отменённая-цель--сперва-задачи-потом-цель)).
|
||||
- **Задача** — то, что мерджится целиком и даёт видимую пользу.
|
||||
- **Вопрос** — решение человека. Не останавливает начатую работу, но **блокирует
|
||||
взятие** задачи в спринт. Живёт внутри файла задачи разделом «Вопросы» и тегом
|
||||
`question`.
|
||||
- **Блокер** — состояние, когда спринт не может продолжаться **ни одной**
|
||||
задачей.
|
||||
- **Спринт** — набор задач, замороженный до его конца. Под одной целью — или
|
||||
**без цели вовсе**, законно: багфикс, техдолг, спринт здоровья. Такой набор
|
||||
собран по работоспособности, а не по направлению, и заводится явно
|
||||
(`sprint start --no-goal`).
|
||||
|
||||
## Вопрос, блокер, необратимое
|
||||
|
||||
| | Что это | Когда спрашиваем | Что останавливает |
|
||||
| --- | --- | --- | --- |
|
||||
| **Вопрос** | решение человека | на сессии, пачкой | взятие задачи в спринт |
|
||||
| **Блокер** | спринт не может продолжаться ни одной задачей | немедленно | всё |
|
||||
|
||||
Право на **необратимое** — третье и отдельное: что именно необратимо, называет
|
||||
`CLAUDE.md` проекта, и спрашивается оно всегда, независимо от спринта.
|
||||
|
||||
**Блокер определяется исходом, а не одновременностью.** Встали разом или
|
||||
высыпались из спринта по одной — если продолжать нечем, это блокер: спринт
|
||||
распускается (`sprint close --dissolve --reason …`), человек спрашивается
|
||||
немедленно. Иначе спринт, из которого задачи вышли поштучно, выглядел бы штатно
|
||||
завершённым, а вопросы тихо ждали бы сессии.
|
||||
|
||||
**Отличать вопрос от застревания.** Правило про остаток принадлежит управлению
|
||||
задачами: оно решает, **сделана задача или вышла**, а это исход планирования, не
|
||||
исполнения. **Ниже канонический текст; пайплайн проекта на него ссылается, а не
|
||||
пересказывает** — два экземпляра одного правила разъезжаются, и разъезжаются
|
||||
незаметно, потому что расхождение видно только на редком входе.
|
||||
|
||||
> Есть остаток, который доводится без ответа, — задача продолжается, вопрос
|
||||
> записывается в файл. Остатка нет — задача выходит из спринта.
|
||||
|
||||
С двумя оговорками, без которых тест ошибается:
|
||||
|
||||
> **Остаток, который материализует нерешённое** — записывает в хранилище,
|
||||
> журнал, витрину **или наружу** состояние, зависящее от неотвеченного вопроса,
|
||||
> — **не остаток**. Решение поднимается до начала записи: откатить запись
|
||||
> дороже, чем подождать ответ, а иногда невозможно. «Наружу» — часть правила, а
|
||||
> не пример: выкладка, публикация и отправка данных третьей стороне не
|
||||
> откатываются тем более.
|
||||
|
||||
> **Пол для остатка:** остаток, из которого пропала польза, названная в «зачем», —
|
||||
> это не сделанная задача, а вышедшая из спринта.
|
||||
|
||||
## Заморозка набора
|
||||
|
||||
**Целей не больше одной.** Названа цель — набор служит ей: задача под чужой
|
||||
целью в спринт не попадает, даже если взять удобно (`sprint take` это и
|
||||
запрещает). **Задача с открытым вопросом в набор не берётся** — это верно всегда.
|
||||
|
||||
**Спринт без цели — законный случай, а не недосмотр.** Багфикс, техдолг,
|
||||
здоровье: работа на работоспособность, а не на направление. Цель не названа —
|
||||
сверять нечего, и в такой набор идёт что угодно готовое к взятию, в том числе
|
||||
задачи под разными целями. Заводится он **явно**, `sprint start --no-goal`:
|
||||
забытый флаг и решение человека иначе неотличимы, а это решение продуктовое.
|
||||
Взамен проверки цели остаётся доклад — спринт без цели **называется таковым и
|
||||
объясняется** одной строкой.
|
||||
|
||||
**Новая работа падает в беклог, а не в идущий спринт.** Решение «врываться или
|
||||
отложить» принимается один раз правилом, а не заново каждый раз. Врывается
|
||||
только два класса:
|
||||
|
||||
1. **Необратимый ущерб** — потеря, порча или утечка данных: то, что не чинится
|
||||
доделкой потом.
|
||||
2. **Сломан общий станок** — красная проверка, на которой стоит определение
|
||||
готовности **всех** задач набора. Это не новая работа, а починка того, на чём
|
||||
делается вся остальная.
|
||||
|
||||
Что в проекте считается необратимым ущербом и что — общим станком, называет
|
||||
`CLAUDE.md` проекта. Не названо — спрашиваем человека, а не решаем сами.
|
||||
|
||||
**Конец спринта** — когда каждая задача набора либо сделана, либо вышла с
|
||||
записанной причиной. Не «все сделаны»: иначе одна застрявшая задача держит
|
||||
спринт бесконечно. Пустой набор закрывается `sprint close` — скрипт не даст
|
||||
закрыть непустой.
|
||||
|
||||
Ведение спринта целиком — исходы задачи, определение готовности, приёмка,
|
||||
доклад — [references/sprint.md](references/sprint.md).
|
||||
|
||||
## Сессия: четыре шага в этом порядке
|
||||
|
||||
Это зависимость, а не список.
|
||||
|
||||
1. **Разбор вопросов.**
|
||||
2. **Разбор прошедшего спринта — про процесс, а не про задачи.** Здесь же оба
|
||||
судьи документов канона на весь канон разом, раз в спринт: `doc-consistency`
|
||||
(документы между собой) и `doc-code-drift` (документы против кода).
|
||||
3. **Переоценка задач** порциями.
|
||||
4. **Выбор цели и набор спринта.** Цель называет человек — либо называет, что
|
||||
этот спринт без цели; набор собирает агент и показывает **до старта работ**.
|
||||
|
||||
Рёбра подписаны тем, что ломается при их нарушении:
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
check["tasks.py check (+ --fix)<br/>результат — строкой в доклад"]
|
||||
s1["1. Разбор вопросов<br/>пачкой, не больше трёх за раз"]
|
||||
s2["2. Разбор прошедшего спринта<br/>про процесс → docs/review.md"]
|
||||
s3["3. Переоценка задач порциями"]
|
||||
s4["4. Цель называет человек,<br/>набор собирает агент"]
|
||||
sprint["спринт: набор заморожен"]
|
||||
|
||||
check --> s1
|
||||
s1 --> s2
|
||||
s2 --> s3
|
||||
s1 -->|"неотвеченный вопрос → переоценка вслепую"| s3
|
||||
s3 -->|"без переоценки набор берётся из протухшего"| s4
|
||||
s4 --> sprint
|
||||
```
|
||||
|
||||
Схема — **сводка**: процедура каждого шага в
|
||||
[references/cadence.md](references/cadence.md), и при расхождении прав текст.
|
||||
|
||||
## Вернулся, а спринт открыт
|
||||
|
||||
Сессия — ритуал **между** спринтами, и шаг 1 предполагает только что закрытый.
|
||||
Вход после перерыва другой, и начинается он не с шага, а с вопроса, свой ли ещё
|
||||
набор:
|
||||
|
||||
1. `tasks.py check` — блок здоровья скажет состояние спринта, число готовых к
|
||||
взятию и залежавшихся; при расхождении раскладки `--fix`.
|
||||
2. Прочитать `SPRINT.md`: цель (или запись, что её нет), состав, дата начала.
|
||||
3. **Развилка, и решает её человек.** Набор всё ещё твой — продолжай спринт, ни
|
||||
сессии, ни переоценки не нужно, они между спринтами. Взялся перечитывать,
|
||||
зачем эти задачи собраны вместе, — набор протух:
|
||||
`sprint close --dissolve --reason …`, недоделанное возвращается в беклог,
|
||||
дальше обычная сессия с шага 1.
|
||||
|
||||
Порога в неделях нет намеренно — почему, в
|
||||
[references/sprint.md](references/sprint.md), «Протухший набор».
|
||||
Середины у развилки тоже нет: «доделаю пару штук и решу» — это работа по набору,
|
||||
которого ты уже не понимаешь.
|
||||
|
||||
Процедура каждого шага, размер и отбор порции, храповик на залежавшихся, формат
|
||||
интерактива и доклад — [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 start --dir D --no-goal # шаг 4: набор без цели, явным флагом
|
||||
python3 $tk sprint take --dir D <слаг> … # шаг 4: набор
|
||||
python3 $tk sprint close --dir D # конец спринта; --dissolve при блокере
|
||||
python3 $tk reopen <слаг> --dir D --reason … # приёмка не сошлась после закрытия
|
||||
```
|
||||
|
||||
`D` — каталог задач проекта, по канону всегда `docs/tasks`; `--dir` передаётся
|
||||
явно каждой командой. Вызов из чужого контекста описан в скилле `tasks`
|
||||
(«Переносимость»). **Коды выхода** — там же: 1 это дрейф в беклоге, 3 это
|
||||
«каталога нет», и ветвиться на них надо по-разному.
|
||||
|
||||
**Слаг спринта заводит `sprint start`** (по умолчанию — дата) и пишет его в
|
||||
`SPRINT.md`; всё заведённое **при открытом спринте** помечается `sprint:<слаг>`
|
||||
автоматически. Поэтому «первая порция — урожай прошедшего спринта» работает без
|
||||
чьей-либо памяти — но ровно до команды `sprint close`, которая `SPRINT.md`
|
||||
очищает. Отсюда порядок: **урожай заводится до закрытия, слаг для сессии берётся
|
||||
из отчёта `sprint close`** ([references/sprint.md](references/sprint.md)).
|
||||
|
||||
Правки задач делаются мутациями (`edit`, `move`, `close`), а не редактором:
|
||||
руками правится только тело файла. Это правило скилла `tasks`, здесь оно не
|
||||
пересказывается.
|
||||
|
||||
## Стимулы, которые процесс создаёт
|
||||
|
||||
Правило, которое можно обойти в свою пользу, будет обойдено.
|
||||
|
||||
**Приёмщик и исполнитель здесь совпадают, и это надо назвать вслух.** Задачу
|
||||
закрывает и двигает по индексам агент-оркестратор — тот же, кто её и сделал.
|
||||
Прежде границу держала механика: моста между плагинами не было, и закрыть задачу
|
||||
пайплайн физически не мог. Теперь мост есть, и защита у трёх обходов ниже —
|
||||
**только текстовая**. Опоры, которые остались настоящими:
|
||||
|
||||
- **независимый отчёт ревью** — артефакт, написанный не исполнителем; по нему
|
||||
сверяют состав прогона и урожай. Где он лежит, знает пайплайн проекта; при
|
||||
конвейере `av-dev-pipeline` это отчёт триажа в
|
||||
`openspec/changes/archive/<id>/review/` (до архивации — `changes/<id>/review/`);
|
||||
- **`SPRINT.md` под git** — `git log -p` показывает, что и когда было закрыто.
|
||||
Работает, только если закрытие **закоммичено**: удаление файла задачи и правка
|
||||
индекса, оставшиеся в рабочем дереве, никакой истории не образуют;
|
||||
- **`reopen <слаг> --reason`** — закрытие не окончательно. Приёмка человеком на
|
||||
сессии его отменяет, и это штатная операция, а не скандал.
|
||||
|
||||
Известные обходы:
|
||||
|
||||
- **Скрыть блокер** — он останавливает всё и выглядит как провал исполнителя.
|
||||
Защита: тест про остаток плюс прямая запись, что **объявление блокера
|
||||
неудачей не считается**.
|
||||
- **Не записать вопрос** на задаче-кандидате, чтобы не вычеркнуть её из
|
||||
ближайшего набора. Защита: вопросы кандидатов разбираются на той же сессии
|
||||
**вне очереди порции**.
|
||||
- **Занизить критерии приёмки**, раз они пол. Защита ослаблена: правит их тот же,
|
||||
кто по ним отчитывается. Остаётся требование, что расхождение критериев с
|
||||
сутью — **дефект критериев, о котором сообщают, а не молча дорабатывают**, и
|
||||
переоценка на сессии, где критерии видит человек.
|
||||
- **Сжать задачу до остатка** и отчитаться «сделана». Защита ослаблена там же.
|
||||
Пол для остатка — польза, названная в «зачем»; проверяет его человек при приёмке,
|
||||
и `reopen` — его инструмент.
|
||||
- **Объявить спринт без цели**, чтобы не задавать человеку продуктовый вопрос:
|
||||
набор без цели берёт что угодно, и собрать его можно молча. Защита: цели нет
|
||||
— это **ответ человека, а не умолчание** (`--no-goal` спрашивается так же, как
|
||||
цель), плюс строка доклада, называющая спринт бесцельным и объясняющая почему.
|
||||
Два бесцельных спринта подряд — предмет разбора процесса, а не мелочь.
|
||||
- **Занизить урожай** — не заводить найденное по ходу. Защита: поимённая сверка
|
||||
с **сохранённым независимым отчётом**, а не с прозой исполнителя. Каждая
|
||||
отложенная находка имеет либо слаг, либо строку «не заведена: причина».
|
||||
Нулевой урожай при непустом отчёте виден сразу.
|
||||
|
||||
**Проект без конвейера ревью — независимого отчёта нет, и это надо сказать, а не
|
||||
обойти молча.** Задачи делались руками или чужим пайплайном, сверять урожай не с
|
||||
чем: остаётся проза исполнителя, то есть тот же взгляд, что и у автора. Тогда
|
||||
защита от занижения урожая **снята**, и доклад спринта обязан нести строку «урожай
|
||||
сверялся с отчётом исполнителя — независимого отчёта в проекте нет». Дальше это
|
||||
решение человека: завести конвейер, принимать выборочной перепроверкой или
|
||||
согласиться с ценой. Молчание здесь хуже любого из трёх исходов.
|
||||
|
||||
Стимулы внутри пайплайна задачи (занизить требования к проверке, пропустить
|
||||
проход) принадлежат ему и защищены там же.
|
||||
|
||||
## Слоты проекта
|
||||
|
||||
Сессия не знает ни языка, ни сборки, ни CI. На часть проектного отвечает своей
|
||||
структурой [канон](../canon/references/canon.md): разбор процесса (шаг 2) живёт
|
||||
в `docs/review.md`, оракулы и «чем краснеет безусловно» — в семантике гейта в
|
||||
`CLAUDE.md`. Остальное проект **дописывает в `CLAUDE.md`**:
|
||||
|
||||
1. **Пайплайн задачи** — чем задача выполняется и что входит в его определение
|
||||
готовности. Сессия требует только форму: пайплайн пройден + критерии приёмки
|
||||
проверены поимённо.
|
||||
2. **Общий станок** — какая проверка, покраснев, врывается в замороженный
|
||||
спринт.
|
||||
3. **Необратимое** — что спрашивается у человека всегда (тот же слот, что у
|
||||
скилла `tasks`; дом один).
|
||||
4. **Ориентир по размеру спринта**, если он замерялся. Умолчание — 5–8 задач, и
|
||||
это **ориентир, а не закон**.
|
||||
|
||||
Слота «куда копируются критерии приёмки» здесь нет намеренно: на него отвечает
|
||||
**пайплайн проекта** — он переносит критерии в описание изменения, когда его
|
||||
заводит. Проект без пайплайна называет своё место сам, в слоте 1.
|
||||
|
||||
Числа проекта (сколько задач в спринте, сколько времени на задачу, каков прирост
|
||||
беклога) — предмет шага 2, а не константы этого скилла.
|
||||
|
||||
## Чего этот скилл не делает
|
||||
|
||||
Не пишет код и не выполняет задачи. Не заводит и не переоформляет задачи сам по
|
||||
себе — формат и содержимое ведёт `tasks` (сессия зовёт его операции). Не решает
|
||||
за человека, какая цель следующая. Не двигает набор идущего спринта.
|
||||
@@ -1,277 +0,0 @@
|
||||
# Сессия: четыре шага
|
||||
|
||||
Одна сессия между спринтами. Порядок шагов — **зависимость, а не список**:
|
||||
переоценивать задачи, не разобрав вопросы, значит переоценивать вслепую; набирать
|
||||
спринт, не переоценив, значит набирать из протухшего.
|
||||
|
||||
Начинается сессия с `tasks.py check` (и `check --fix`, если дрейф накопился) —
|
||||
результат идёт строкой в доклад.
|
||||
|
||||
## Шаг 1. Разбор вопросов
|
||||
|
||||
`tasks.py list --questions` — всё, что накопилось. Вопрос это решение человека,
|
||||
и разбирается он **пачкой**, а не по одному, как только возник: по одному —
|
||||
это дёрганье, пачкой — это сессия.
|
||||
|
||||
Порядок по каждому вопросу:
|
||||
|
||||
1. **Проверь, не отвечен ли он уже** — решением, документом, соседним
|
||||
изменением, самим ходом прошедшего спринта. Отвеченный вопрос не выносится
|
||||
человеку: это самая частая находка и она не требует ничьего решения.
|
||||
2. **Сформулируй развилку** с вариантами и последствием каждого, рекомендация —
|
||||
первым вариантом.
|
||||
3. **Вынеси пачкой** через `AskUserQuestion`, не больше трёх за раз.
|
||||
4. **Запиши ответ в тело задачи, опустоши раздел «Вопросы»**, сними тег
|
||||
(`edit <slug> --rm-tag question`), **перепиши «зачем»**: «Решено:
|
||||
…» на вопрос «почему это лежит в беклоге» уже не отвечает. Опустошение
|
||||
раздела — не уборка, а условие взятия: правило и причина в скилле `tasks`,
|
||||
[references/task-format.md](../../tasks/references/task-format.md).
|
||||
|
||||
**Вопросы на задачах-кандидатах разбираются вне очереди порции** — здесь же, на
|
||||
этой сессии, даже если сама задача в порцию переоценки не попала. Иначе правило
|
||||
«задача с открытым вопросом в набор не берётся» создаёт стимул вопрос не
|
||||
записывать, лишь бы не вычеркнуть задачу из ближайшего спринта.
|
||||
|
||||
## Шаг 2. Разбор прошедшего спринта — про процесс, а не про задачи
|
||||
|
||||
Не «что мы сделали» (это доклад спринта, он уже был), а:
|
||||
|
||||
- **что сломалось в процессе и почему не поймали** — промах, доехавший до конца;
|
||||
- **что оказалось дороже, чем выглядело при заведении** — не число, а сам факт и
|
||||
причина: чего не было видно в постановке;
|
||||
- **какие правила не сработали или сработали не так** — в том числе правила
|
||||
этого плагина.
|
||||
|
||||
Замеров процесс не ведёт намеренно: оценки в очках и velocity не взяты
|
||||
(«[Почему не Scrum](../SKILL.md#почему-не-scrum)»), а спринт ограничен объёмом, а
|
||||
не временем — сравнивать «сколько заняло» не с чем. Разбор здесь качественный, и
|
||||
это не упущение.
|
||||
|
||||
**Артефакт обязателен.** Вывод, оставшийся в контексте сессии, не существует:
|
||||
следующая сессия его не увидит. Дом у него один и известен из канона —
|
||||
**`docs/review.md`**: вывод про конвейер и про то, что перестали проверять, идёт
|
||||
в раздел настройки, вывод про воспроизведённый дефект — в журнал. Решение с
|
||||
долгим следом — в `docs/adr/`.
|
||||
|
||||
Отдельным ритуалом ретроспектива не выделяется: процесс личный,
|
||||
синхронизировать некого.
|
||||
|
||||
**Здесь же зовутся оба судьи документов** — на весь канон разом, а не на пачку,
|
||||
отобранную работой:
|
||||
|
||||
- **`doc-consistency`** — согласованность документов между собой и с openspec:
|
||||
факт в двух домах, прямое противоречие, поведение в `architecture.md` вместо
|
||||
спек, ADR без парного статуса при замене, число без провенанса;
|
||||
- **`doc-code-drift`** — сверка с кодом по закрытому перечню фактов: имя основной
|
||||
ветки, команды, пути, внешние зависимости поимённо, настройки с числовым
|
||||
значением, единые точки проекта, capability.
|
||||
|
||||
Раз в спринт, а не чаще. Дорог из них по-настоящему первый — `doc-consistency`
|
||||
на `opus`: он сличает утверждения двух документов, и это суждение. Второй с
|
||||
недавних пор на `sonnet` — у него закрытый перечень фактов и команда на каждый, —
|
||||
но он читает репозиторий целиком, и дешёвым от смены модели не стал. Но и не
|
||||
реже — **спринт это ровно то, что двигает код и документы**:
|
||||
переименованная цель сборки, ушедшая зависимость, второй способ делать то, что
|
||||
обзор объявил единственным; факт, дописанный в `architecture.md`, уже живущий в
|
||||
`CLAUDE.md`. Протухшее и раздвоившееся неотличимо от свежего, и по нему принимают
|
||||
решения, пока кто-нибудь не наткнётся.
|
||||
|
||||
**Пачка — весь канон, и это не расточительство, а охват.** Когда пачку отбирала
|
||||
работа, без присмотра оставалось ровно то, чего работа не касалась: правка,
|
||||
отменившая решение, живёт в одном документе, а парный статус нужен в другом.
|
||||
Канон мал, раз в спринт он читается целиком.
|
||||
|
||||
Находки обоих — обычный материал переоценки: строка на замену идёт в документ
|
||||
сразу, работа больше чем на абзац становится задачей типа `chore`. **Позвал —
|
||||
скажи в докладе, кого именно позвал, и приложи границы покрытия**; не позвал —
|
||||
скажи и это, иначе доклад читается как «сверено».
|
||||
|
||||
## Шаг 3. Переоценка задач
|
||||
|
||||
Цель — выкинуть то, что перестало быть задачей, и вернуть остальному честное
|
||||
состояние. Не «пересмотреть всё», а «пересмотреть порцию до конца».
|
||||
|
||||
### Порция и правило остановки
|
||||
|
||||
Тридцать задач за один заход — это усталость и штамповка: последние десять
|
||||
получат «оставить» не потому, что живы, а потому, что сессия затянулась.
|
||||
|
||||
- **5–8 задач за порцию.** Размер обоснован усталостью, а не пропускной
|
||||
способностью, и менять его не надо — **надо брать несколько порций за
|
||||
сессию**.
|
||||
- **Сколько порций:** не меньше `⌈урожай прошедшего спринта / 8⌉`. Урожай — это
|
||||
задачи, заведённые за спринт; при урожае в 15 это две-три порции.
|
||||
- **Отбор порций по порядку:**
|
||||
1. **урожай спринта** — `list --tag sprint:<слаг>`: свежезаведённое ещё не
|
||||
проходило ни одной проверки на нужность. Тег на задачах проставлен
|
||||
автоматически при заведении — руками не метят и не вспоминают. **Слаг
|
||||
берётся из отчёта `sprint close`, а не из `SPRINT.md`:** сессия идёт после
|
||||
закрытия, а закрытие этот файл очищает;
|
||||
2. дальше **по залежалости** — `list --stale`;
|
||||
3. по потребности — одна секция целиком, один тег (партия ревью), одна цель
|
||||
(`--goal`), список от пользователя.
|
||||
- **Останавливайся на границе порции**, даже если «ещё чуть-чуть осталось».
|
||||
Между порциями — промежуточный доклад.
|
||||
|
||||
### Что делать с каждой задачей
|
||||
|
||||
Сперва то, что не требует ничьего решения:
|
||||
|
||||
1. **Проверь, не сделано ли уже.** Задача, реализованная попутно в соседнем
|
||||
изменении, — самая частая находка. Смотри код, документацию, историю коммитов
|
||||
по ключевым словам. Удаление «как реализованной» деструктивно и без следа
|
||||
(в `REJECTED.md` реализованные не пишутся), поэтому порог улики жёсткий:
|
||||
`close <slug> --implemented` только имея **конкретный коммит или строку
|
||||
документа**, закрывающие задачу, и ссылка идёт в доклад. Есть лишь косвенные
|
||||
признаки — не удаляй сам, вынеси в пачку вопросов. Сделана частично → задача
|
||||
сжимается до остатка: тело правишь редактором, заголовок и «зачем» — через
|
||||
`edit`.
|
||||
2. **Проверь, не отменена ли решением.** Документ, ADR или архивное изменение
|
||||
мог закрыть вопрос иначе — тогда `close <slug> --reason "<ссылка на
|
||||
решение>"`. Задача закрывается не только коммитом.
|
||||
3. **Проверь пересечения.** Две задачи об одном — содержимое в одну, вторую
|
||||
`close <slug> --reason "слита с <другой-слаг>"`. Смотри **шире порции**:
|
||||
интейк дедуплицирует новое против существующего, но никогда не
|
||||
пересматривает уже лежащее, и две задачи с одной причиной могут лежать рядом
|
||||
месяцами.
|
||||
4. **Пере-кластеризуй по общей причине.** Несколько задач, оказавшихся симптомами
|
||||
одного дефекта, сливаются в одну — это находка, которую интейк дать не мог.
|
||||
5. **Гигиена полей** — протухшее «зачем», вопрос в прозе, снятый ответ, свойство
|
||||
репозитория в рамках, предписание процесса в теле, тип, разошедшийся с
|
||||
задачей, границы вместо реализации в разделе «Затрагивает». Список и правила —
|
||||
в скилле `tasks`. **Переоценка — то самое место, где беклог добирает тип и
|
||||
разделы его схемы:** требовать их на входе значило бы выгонять в заметки то,
|
||||
что должно лежать задачей, а к взятию в спринт они уже обязательны. Блок
|
||||
здоровья `check` печатает, сколько записей готово к взятию, — по этому числу
|
||||
и видно, добрала переоценка или нет.
|
||||
|
||||
Затем — то, что решает пользователь:
|
||||
|
||||
6. **Жива ли она вообще.** Контекст мог измениться: ушла зависимость, отпал
|
||||
сценарий, обошли иначе. Здесь и звучит вопрос, выкидывать ли.
|
||||
7. **Та ли цель — и нужна ли она вообще.** Приоритетов нет, и «повысить» нечего
|
||||
— вместо повышения задача **меняет цель** (`edit <slug> --goal <другой>`) или
|
||||
входит в ближайший набор. `feature`, которой не находится цель, — кандидат
|
||||
на выход: новая возможность вне цели это возможность, которой никто не
|
||||
заказывал. Операционной задаче (`fix`, `chore`, `research`) цель не нужна, и
|
||||
выдумывать её здесь не надо.
|
||||
|
||||
**Отменяется и сама цель** — когда замысел оказался неверен, а не когда
|
||||
задача выбрала не ту. Тогда порция расширяется до всех задач этой цели: каждую
|
||||
либо закрыть своей причиной, либо перевесить на другую цель, и только потом
|
||||
закрыть цель. Порядок и почему он такой —
|
||||
[task-goal.md](../../tasks/references/task-goal.md#отменённая-цель--сперва-задачи-потом-цель).
|
||||
Здесь этому и место: отмена цели это разбор её задач, а разбор задач — этот
|
||||
шаг.
|
||||
8. **Задача ли это по-прежнему.** Не проходит тест «готова к взятию» → `edit
|
||||
<slug> --type research` и опустошённый раздел «Вопрос», то есть сырьё; дальше
|
||||
штурм. Разрослась → это несколько задач под той же целью, дальше
|
||||
декомпозиция.
|
||||
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` — вынести распознавание в сервис-спутник
|
||||
> - Понизить до сырья (`--type research`) *(рекомендую)* — не проходит тест «готова к взятию»
|
||||
> - Оставить задачей
|
||||
|
||||
Каждый вариант несёт причину — ту самую, что уедет в `--reason`. Ответы применяй
|
||||
сразу и, если в порции осталось ещё, следующей итерацией показывай следующие ≤3.
|
||||
|
||||
## Шаг 4. Выбор цели и набор спринта
|
||||
|
||||
1. **Покажи состояние проекта**: секцию `Готово` (что приложение уже умеет —
|
||||
это половина ответа на «где мы»), затем `Запланировано` с обоснованием
|
||||
очереди, `Направления`, и
|
||||
по каждой цели-кандидату — сколько под ней задач без открытых вопросов
|
||||
(`list --goal <слаг>`). Цель без готовых задач набором не станет: её сперва
|
||||
надо декомпозировать.
|
||||
2. **Цель называет человек** — либо называет, что цели не будет. Это
|
||||
продуктовое решение, а не механика: агент предлагает и объясняет, но не
|
||||
выбирает. **Оба ответа законны**, и «без цели» — такой же ответ, как слаг:
|
||||
спринт бывает под багфикс, под техдолг, под здоровье проекта. Спрашивается он
|
||||
так же, как цель, и в отдельный вопрос не выносится: это один и тот же вопрос
|
||||
«подо что набираем».
|
||||
3. **Набор собирает агент** — `sprint start --goal <слаг>` (или `sprint start
|
||||
--no-goal`), затем `sprint take …`. Скрипт не даст взять цель, задачу с чужой
|
||||
целью, с открытым вопросом, без типа и **без разделов, которых требует её
|
||||
тип** (у `fix` это в том числе `Воспроизведение`, у `research` — `Вопрос` и
|
||||
`Куда ляжет ответ`, и сырьё поэтому не берётся вовсе). Задача без цели (`fix`,
|
||||
`chore`, `research`) берётся свободно — операционная работа входит в набор
|
||||
помимо его цели. **В спринте без цели чужой цели нет вовсе**: сверять не с
|
||||
чем, берётся что угодно готовое, и единственной защитой остаётся показ набора
|
||||
человеку.
|
||||
4. **Набор показывается человеку до старта работ.** Показ — это и есть момент
|
||||
заморозки: после него набор не двигается. **В показе называется состав по
|
||||
типам** — три `fix` и ни одной `feature` под целью развития это разговор про
|
||||
цель, а не про набор, и увидеть его надо до заморозки, а не в докладе по
|
||||
итогам. **У набора без цели показ — единственная проверка состава**: скрипту
|
||||
там отказывать не по чему, и «что угодно готовое» превращается в осмысленный
|
||||
набор только глазами человека.
|
||||
|
||||
Здесь же последний дешёвый момент заметить **разнородную задачу**: раздел
|
||||
«Затрагивает» показывает границы до того, как заведено предложение об
|
||||
изменении. Строка, которая одна тянет задачу на метку выше остального
|
||||
перечня, — кандидат на разрез (шов — в `tasks`, `references/split.md`).
|
||||
Замеченная здесь, она стоит одного `edit`; замеченная на ревью — выброшенного
|
||||
предложения.
|
||||
5. Задача, которой для взятия не хватает только разделов её типа, дописывается
|
||||
здесь же — критерии с оракулами, перечень границ, шаги воспроизведения. Но
|
||||
если для этого нужен ответ человека, это вопрос, и задача в набор не идёт.
|
||||
|
||||
**Размер — ориентир, а не закон:** 5–8 задач. Можно взять больше, можно меньше —
|
||||
набор под цель важнее круглого числа; одна крупная задача спринтом тоже бывает.
|
||||
|
||||
## Доклад сессии
|
||||
|
||||
- Что просмотрено: N из M, сколько порций, по какому признаку отобраны.
|
||||
- Вопросы: разобрано N, из них отвечено без человека N, снято тегов N.
|
||||
- Разбор процесса: что записано и куда.
|
||||
- Сверка документов с кодом: звался ли `doc-code-drift`, что проверено из
|
||||
названного, что разошлось.
|
||||
- Изменения списком: удалено как реализованное (со ссылками), ушло без
|
||||
реализации (с причинами), понижено до сырья, слито, сменило тип или цель.
|
||||
- Новый спринт: цель — **или строка «без цели» с объяснением, почему** (багфикс,
|
||||
техдолг, здоровье), — набор со слагами, дата, состав по типам.
|
||||
- **Границы покрытия**: сколько задач не трогали и какие именно секции, теги или
|
||||
цели остались — иначе доклад читается как «беклог разобран».
|
||||
- `tasks.py check` после правок — результат строкой.
|
||||
@@ -1,199 +0,0 @@
|
||||
# Ведение спринта
|
||||
|
||||
Спринт — набор задач, замороженный до его конца: под одну цель или **без цели**
|
||||
(багфикс, техдолг, здоровье — это законно, `sprint start --no-goal`). Здесь то,
|
||||
что происходит **внутри** спринта: как задача заканчивается, что считается сделанным,
|
||||
кто принимает и что идёт в доклад. Как спринт набирается — шаг 4 в
|
||||
[cadence.md](cadence.md).
|
||||
|
||||
## Наблюдаемые исходы задачи
|
||||
|
||||
Как они достигаются — дело пайплайна проекта. Сессия знает только исход и его
|
||||
след.
|
||||
|
||||
- **Сделана** — по определению готовности ниже. `close <slug> --implemented`:
|
||||
файл и строка удаляются, следом остаётся коммит. **Закрывает агент-оркестратор
|
||||
последним шагом пайплайна, после коммита; приёмка человеком идёт позже и
|
||||
отменяется `reopen`** — см. «Кто и когда закрывает».
|
||||
- **Вышла из спринта** — `sprint drop <slug> --reason …`: возвращается в беклог
|
||||
с вопросом в файле и **без живого незакоммиченного предложения** — иначе при
|
||||
следующем взятии оно столкнётся с новым. Наработки, которые жалко терять,
|
||||
переезжают в тело задачи текстом.
|
||||
- **Оказалась крупнее задачи** — распознаётся **до того, как под неё заведено
|
||||
предложение об изменении**, иначе его придётся выбрасывать. Выходит из набора,
|
||||
уходит на декомпозицию; спринт продолжается остальными, части заводятся под той
|
||||
же целью (у спринта без цели — без неё) и в замороженный набор не добавляются.
|
||||
- **Отменена решением по ходу** — `close <slug> --reason "<ссылка на решение>"`
|
||||
прямо из спринта. Это редкий, но законный исход, и он называется в докладе.
|
||||
|
||||
**Конец спринта** — когда по каждой задаче набора наступил один из исходов. Не
|
||||
«все сделаны»: иначе одна застрявшая задача держит спринт бесконечно. Затем
|
||||
`sprint close`.
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
take["sprint take — задача в наборе"]
|
||||
done["сделана<br/>close --implemented"]
|
||||
out["вышла<br/>sprint drop --reason"]
|
||||
epic["крупнее задачи<br/>распознаётся до заведения change"]
|
||||
cancel["отменена решением по ходу<br/>close --reason"]
|
||||
all{"по каждой задаче набора<br/>наступил исход?"}
|
||||
harvest["урожай заводится интейком tasks"]
|
||||
close["sprint close"]
|
||||
dissolve["sprint close --dissolve --reason<br/>недоделанное — в беклог"]
|
||||
|
||||
take --> done
|
||||
take --> out
|
||||
take --> epic
|
||||
take --> cancel
|
||||
done --> all
|
||||
out --> all
|
||||
epic --> all
|
||||
cancel --> all
|
||||
all -->|да| harvest
|
||||
harvest -->|"тег sprint: ставится, пока SPRINT.md не очищен"| close
|
||||
take -->|"продолжать нечем ни одной задачей — блокер"| dissolve
|
||||
done -->|"приёмка не сошлась: reopen --reason"| take
|
||||
```
|
||||
|
||||
Два ребра на схеме — те, где порядок обязателен и нарушается молча: **урожай до
|
||||
`sprint close`** (после команды автотег уже не поставится) и **блокер в обход
|
||||
исходов** (спринт распускается, а не ждёт).
|
||||
|
||||
Схема — **сводка**: определение готовности и правила приёмки ниже, и при
|
||||
расхождении прав текст.
|
||||
|
||||
**Урожай заводится при закрытии спринта, а не при закрытии задачи.** Это
|
||||
обязанность закрывающего: пройти по спискам находок от исполнителей и завести
|
||||
недостающее интейком скилла `tasks` — с дедупликацией и картой человеку. Заводимое
|
||||
метится тегом спринта само (`sprint:<слаг>`), поэтому первая порция следующей
|
||||
сессии поднимается одной командой `list --tag sprint:<слаг>`. Спринт, закрытый
|
||||
без этого шага, оставляет находки жить в отчётах — то есть нигде.
|
||||
|
||||
**Порядок здесь обязателен: урожай заводится ДО команды `sprint close`.**
|
||||
Автотег ставится по слагу из `SPRINT.md`, а `sprint close` этот файл очищает;
|
||||
заведённое после команды остаётся без тега и в первую порцию следующей сессии
|
||||
не попадёт — молча, потому что пустой `list --tag` выглядит как «урожая не
|
||||
было». Если так уже вышло, тег ставится руками: `add … --tag sprint:<слаг>`,
|
||||
слаг берётся из отчёта `sprint close`.
|
||||
|
||||
**Провал спринта.** Сработал блокер — спринт распускается (`sprint close
|
||||
--dissolve --reason …`), недоделанное возвращается в беклог, новый набор
|
||||
делается после ответа человека. Спринт не «ждёт»: ждать может человек, а
|
||||
замороженный набор, который нельзя двигать, только мешает.
|
||||
|
||||
**Протухший набор — второй законный повод роспуска.** Работа стояла, и человек
|
||||
вернулся к спринту, состав которого уже не держит в голове. Тем же роспуском:
|
||||
`sprint close --dissolve --reason "работа стояла с <когда>"`, недоделанное в
|
||||
беклог, новый набор — после переоценки, а не поверх старого.
|
||||
|
||||
Порога в неделях нет и не будет: счётчик простоя пришлось бы вести руками, а
|
||||
решает всё равно человек. Признак — не срок, а **что набор перестал быть твоим**:
|
||||
взялся перечитывать, зачем эти задачи вместе, — он протух. Заморозка тут не
|
||||
мешает, она запрещает *двигать* набор, а не распустить его целиком.
|
||||
|
||||
## Определение готовности
|
||||
|
||||
Задача засчитывается сделанной, когда верно **всё**:
|
||||
|
||||
1. **Пайплайн задачи пройден до конца** — со своим определением готовности, за
|
||||
которое отвечает проект: проверки, состав ревью, документация, коммит. Здесь
|
||||
оно не пересказывается и не подменяется — **форма фиксирована, содержание
|
||||
даёт `CLAUDE.md` проекта**. Пайплайна нет, задача сделана руками — условие
|
||||
читается как «проверки проекта зелёные и изменение влито».
|
||||
2. **Критерии приёмки проверены поимённо** — каждый со своим оракулом, исход по
|
||||
каждому назван. Это единственное, что добавляет управление задачами: пайплайн
|
||||
отвечает «сделано по правилам», критерии — «сделано то, что заказывали».
|
||||
3. **Находки по ходу отданы списком** — исполнитель обязан их **назвать**
|
||||
(каждую, с пометкой «заведена / не заведена: причина»), но **не обязан
|
||||
заводить**: заведение интерактивно — оно требует дедупликации против беклога
|
||||
и кладбища, а ещё решений человека. Обязанность **завести урожай** — на
|
||||
закрытии спринта, ниже. Иначе автономный исполнитель оказался бы разом и
|
||||
обязан завести задачи, и не вправе сделать это в одиночку.
|
||||
|
||||
### Кто и когда закрывает
|
||||
|
||||
**Задачу закрывает агент-оркестратор — тот же, кто её и сделал**, последним шагом
|
||||
пайплайна, после коммита. Порядок:
|
||||
|
||||
1. пайплайн доводит задачу до коммита;
|
||||
2. **после коммита** зовёт `Skill av-dev-pm:tasks` и закрывает задачу
|
||||
(`close <slug> --implemented`); строка уходит из `SPRINT.md`;
|
||||
3. **докладывает исход и по каждому критерию — оракул и наблюдаемый исход.**
|
||||
Это доклад приёмщику, а не отметка «принято».
|
||||
|
||||
**Приёмщик и исполнитель здесь совпадают, и это принято сознательно** — цена
|
||||
названа в `SKILL.md`, раздел «Стимулы». Поэтому закрытие **не окончательно**, а
|
||||
доклад по критериям — не формальность: он единственное, по чему приёмка вообще
|
||||
возможна.
|
||||
|
||||
**Порядок «коммит, потом закрытие» обязателен.** Закрытие удаляет файл задачи;
|
||||
упавший коммит после закрытия оставил бы задачу закрытой без единого следа
|
||||
работы.
|
||||
|
||||
**Само закрытие тоже коммитится, отдельным коммитом.** Удаление файла задачи и
|
||||
правка индекса — правки в рабочем дереве; пока они не в истории, `SPRINT.md`
|
||||
ничего не показывает, а `reopen` восстанавливает текст из `HEAD` в узком окне:
|
||||
его закроет первый посторонний коммит. Сообщение про учёт, а не про
|
||||
работу: `закрыта задача <slug>`.
|
||||
|
||||
**Дорога назад существует и обязана быть названа.** Человек на сессии сверил
|
||||
критерии, и приёмка не сошлась — `tasks.py reopen <slug> --reason "приёмка не
|
||||
сошлась: …"`:
|
||||
файл восстанавливается из истории git, строка возвращается в набор идущего
|
||||
спринта (или в беклог, если спринта нет), строка кладбища снимается. Тело
|
||||
восстанавливается **на момент удаления** — всё, что было дописано позже, живёт
|
||||
только в коммите задачи, и это называется в докладе.
|
||||
|
||||
### Кто и по чему принимает
|
||||
|
||||
Три условия, без которых пункт про критерии не исполняется никем:
|
||||
|
||||
1. **Критерии переживают файл задачи.** Файл удаляется при закрытии, поэтому
|
||||
критерии копируются туда, где их увидит приёмщик. Куда именно — **отвечает
|
||||
пайплайн проекта, а не слот в `CLAUDE.md`**: он переносит их в `tasks.md`
|
||||
изменения, когда заводит change. Проект без пайплайна называет своё место
|
||||
сам.
|
||||
2. **Принимает человек на сессии, а не отдельный агент.** Исполнитель и приёмщик
|
||||
в момент закрытия **не разведены** (решение о снятии и его
|
||||
цена — в `SKILL.md`, «Стимулы»). Опоры остались три: **сохранённый независимый
|
||||
отчёт ревью** (при конвейере `av-dev-pipeline` — отчёт триажа в
|
||||
`openspec/changes/archive/<id>/review/`, до архивации — `changes/<id>/review/`),
|
||||
`SPRINT.md` под git и `reopen`. Переоценка на сессии и есть момент, когда
|
||||
критерии видит не исполнитель. **Конвейера ревью в проекте нет — первой опоры
|
||||
нет тоже**, и это называется строкой доклада, а не обходится молча
|
||||
(`SKILL.md`, «Стимулы»).
|
||||
3. **Расхождение — дефект критериев.** Приёмщик правит критерии и возвращает
|
||||
задачу исполнителю **в этом же спринте**: ответ есть, остаток есть, по тесту
|
||||
про остаток это не выход из спринта.
|
||||
|
||||
## Что врывается в замороженный набор
|
||||
|
||||
Только два класса — правило и его обоснование в SKILL.md. Здесь механика:
|
||||
|
||||
- вторжение **не добавляет** задачу в набор: `SPRINT.md` остаётся тем набором,
|
||||
который заморозили и показали. Внеплановая работа делается и называется в
|
||||
докладе отдельной строкой «внеплановое: что и почему»;
|
||||
- если внеплановое требует больше пары часов, честнее распустить спринт, чем
|
||||
делать вид, что набор соблюдается;
|
||||
- всё остальное падает в беклог через обычный интейк и ждёт сессии.
|
||||
|
||||
## Доклад в конце спринта
|
||||
|
||||
Проверяемые якоря, а не пересказ:
|
||||
|
||||
- **Цель спринта** — или строка «спринт без цели» с тем, чем он был (багфикс,
|
||||
техдолг, здоровье): у бесцельного набора это единственное место, где состав
|
||||
вообще объясняется. И по каждой задаче набора: **хеш коммита**, дословный
|
||||
исход проверок проекта, **исход по каждому критерию приёмки**.
|
||||
- **Какие развилки решались** и чем обоснованы.
|
||||
- **Урожай:** сколько задач заведено, какие вопросы накопились, что вышло из
|
||||
спринта и почему, что было внеплановым.
|
||||
- **Поимённая сверка урожая** с независимыми отчётами ревью: каждая отложенная
|
||||
находка имеет либо слаг, либо строку «не заведена: причина». Нулевой урожай при
|
||||
непустом отчёте — сигнал, а не благополучие. **Отчётов нет** (проект без
|
||||
конвейера ревью) — сверять не с чем, и строка доклада говорит именно это, а не
|
||||
«сверено».
|
||||
- **Созрела ли порция для сессии.** Решение звать — человека, напоминание —
|
||||
обязанность агента: `⌈урожай / 8⌉` порций.
|
||||
- **Границы покрытия** сжатой строкой: что в этом спринте не проверялось вовсе.
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "av-dev-tasks",
|
||||
"description": "Задачи и цели каталогом markdown-файлов: одна запись — файл в items/ плюс строка ровно в одном индексе, у записи тип (goal, feature, fix, chore, research), и тип решает, каких разделов она требует и что с ней можно делать. Заведение из диалога с фильтром и дедупом, разбор находок ревью в задачи, декомпозиция на независимо полезные части, гигиена полей, согласованность индексов скриптом tasks.py. Приоритет — явный порядок строк в беклоге, и расставляет его скилл groom: интерактивный разбор на два вопроса, что сейчас самое важное и что перестало быть важным. Готовность записи к работе проверяет команда ready. Записи вычитывают два прохода: task-form (форма записи) и task-wording (язык). Канон документов ведёт плагин av-dev-docs, он опционален. Задач не выполняет — этим занимается конвейер проекта.",
|
||||
"author": {
|
||||
"name": "Anton Vakhrushev",
|
||||
"email": "anwinged@gmail.com"
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: task-form
|
||||
description: "Проверка формы записи каталога задач по существу: тип, разошедшийся с содержанием записи, форма заголовка по типу (цель — что приложение будет уметь, задача — что нужно сделать, разведка — о чём она), «зачем», пересказывающее заголовок вместо состояния и боли, раздел «Затрагивает» с замыслом вместо границ, критерий приёмки с оракулом только на словах, предписание процесса в теле, и связь задачи со строкой «Завершения» её цели. Читает файл цели, на которую ссылается задача. Отдаёт готовые формулировки на замену и ничего не правит сам. Язык текста (залог, оценки, стоп-слова, англицизмы) смотрит отдельный агент doc-wording. Использовать после заведения или разбора пачки записей, до взятия в спринт и на переоценке. Только чтение."
|
||||
description: "Проверка формы записи каталога задач по существу: тип, разошедшийся с содержанием записи, форма заголовка по типу (цель — что приложение будет уметь, задача — что нужно сделать, разведка — о чём она), «зачем», пересказывающее заголовок вместо состояния и боли, раздел «Затрагивает» с замыслом вместо границ, критерий приёмки с оракулом только на словах, предписание процесса в теле, и связь задачи со строкой «Завершения» её цели. Читает файл цели, на которую ссылается задача. Отдаёт готовые формулировки на замену и ничего не правит сам. Язык текста (залог, оценки, стоп-слова, англицизмы) смотрит отдельный агент task-wording. Использовать после заведения или разбора пачки записей, при проверке готовности перед взятием в работу (tasks.py ready) и на груминге. Только чтение."
|
||||
tools: Read, Grep, Glob
|
||||
model: sonnet
|
||||
color: green
|
||||
@@ -15,7 +15,7 @@ color: green
|
||||
человек со скиллом `tasks`.
|
||||
|
||||
Границу с языком держи твёрдо. **Залог, оценки, стоп-слова, англицизмы, жаргон**
|
||||
— у агента `doc-wording`, и тебе они не поручены даже там, где бросаются в
|
||||
— у агента `task-wording`, и тебе они не поручены даже там, где бросаются в
|
||||
глаза: две проверки одного места расходятся и начинают спорить. Увидел — скажи
|
||||
одной строкой в конце доклада, не находкой. Исключение ровно одно: если
|
||||
неудачное слово стоит **в заголовке** и мешает ему ответить на вопрос своего
|
||||
@@ -27,7 +27,7 @@ color: green
|
||||
|
||||
## Что тебе дают
|
||||
|
||||
Список файлов записей (`docs/tasks/items/<slug>.md`) или каталог задач целиком.
|
||||
Список файлов записей (`tasks/items/<slug>.md`) или каталог задач целиком.
|
||||
Каталог тебе нужен и сам по себе: задача несёт тег `goal:<слаг>`, и **файл цели
|
||||
ты открываешь**, иначе седьмое правило не проверить.
|
||||
|
||||
@@ -124,18 +124,23 @@ color: green
|
||||
|
||||
Не своё бывает двух разных родов, и поступают с ними по-разному.
|
||||
|
||||
**Чужому подрядчику — строкой в границах покрытия.** Язык у `doc-wording`;
|
||||
**Чужому подрядчику — строкой в границах покрытия.** Язык у `task-wording`;
|
||||
согласованность документов канона между собой у `doc-consistency`, их
|
||||
соответствие коду у `doc-code-drift` — до задач эти двое не доходят вовсе, но
|
||||
если ты открыл цель и увидел расхождение в самом документе, оно их. Увидел —
|
||||
назови в конце одной строкой, чтобы находка не пропала, но находкой не оформляй.
|
||||
|
||||
**Машинной проверке — вообще ничего.** Всё, что ловит `tasks.py check` (наличие
|
||||
разделов, число критериев, состав и написание секций, теги, тег `question` при
|
||||
непустом разделе «Вопросы», согласованность индексов, битые ссылки, форма
|
||||
заголовка как строки), **не пиши даже строкой**: это не потерянная находка, а
|
||||
уже проверенное. Повторять машинную проверку словами — заводить второй дом для
|
||||
одного правила.
|
||||
**Машинной проверке — вообще ничего.** Всё, что ловит `tasks.py check` (состав и
|
||||
написание секций, теги, тег `question` при непустом разделе «Вопросы»,
|
||||
согласованность индексов, битые ссылки, форма заголовка как строки), **не пиши
|
||||
даже строкой**: это не потерянная находка, а уже проверенное. Повторять машинную
|
||||
проверку словами — заводить второй дом для одного правила.
|
||||
|
||||
**Наличие разделов и число критериев `check` поимённо не называет** — он считает
|
||||
их строкой здоровья, а поимённо судит `tasks.py ready` на входе в работу.
|
||||
Отсутствующий раздел сам по себе всё равно не твоя находка (её увидит `ready`);
|
||||
твоя — раздел, который **есть и лжёт**: границы вместо замысла, критерий с
|
||||
оракулом только на словах.
|
||||
|
||||
**Содержание работы**: нужна ли задача, верно ли выбрана цель, не крупна ли она,
|
||||
достаточна ли декомпозиция. Седьмое правило подходит к этому близко и
|
||||
@@ -143,7 +148,7 @@ color: green
|
||||
|
||||
## Порог вмешательства
|
||||
|
||||
<!-- копия: порог-правки из av-dev-pm/skills/canon/references/language.md -->
|
||||
<!-- копия: порог-правки из shared/language.md -->
|
||||
|
||||
**Правка без нарушенного правила не делается.** Текст, переписанный «чтобы
|
||||
звучало лучше», обесценивает список замечаний: когда половина из них вкусовая,
|
||||
@@ -0,0 +1,259 @@
|
||||
---
|
||||
name: task-wording
|
||||
description: "Вычитка языка записей каталога задач по информационному стилю — задачи, цели, строки индексов и причины отказа. Смотрит отглагольные существительные и страдательный залог, оценку без факта, стоп-слова и канцелярит, «одна мысль — одно предложение», англицизм при живом русском слове, жаргон и метафоры вместо прямого называния, термин, которого нет в документах проекта, транслит в слаге. Отдаёт готовые формулировки на замену и ничего не правит сам. Форму записи (заголовок по типу, «зачем», границы, оракулы) смотрит отдельный агент task-form, документы проекта вычитывает doc-wording. Использовать после заведения или разбора пачки записей, до взятия в работу и на переоценке беклога. Только чтение."
|
||||
tools: Read, Grep, Glob
|
||||
model: sonnet
|
||||
color: green
|
||||
---
|
||||
|
||||
Ты — **вычитка языка записей каталога задач**: задач, целей, строк индексов и
|
||||
причин отказа. Оптика — слова и фразы, а не то, что запись описывает: ты не
|
||||
судишь, нужна ли задача, верно ли выбрана цель и правильно ли запись оформлена.
|
||||
|
||||
Границу держи твёрдо, и она у тебя одна. **Форму записи** — тип, заголовок по
|
||||
типу, «зачем» вместо пересказа, раздел «Затрагивает», годность оракулов, связь
|
||||
со строкой «Завершения» цели — смотрит `task-form`, и тебе она не поручена даже
|
||||
там, где бросается в глаза. Отсюда же исключение, и оно **против** тебя:
|
||||
неудачное слово **в заголовке** судит `task-form`, потому что заголовок целиком
|
||||
его. Увидел не по своей части — скажи одной строкой в конце доклада, не
|
||||
находкой: две проверки одного места расходятся и начинают спорить.
|
||||
|
||||
**Документы проекта — не твои**: их язык вычитывает `doc-wording`. Ты их
|
||||
читаешь, но только как словарь — по ним проверяется, известен ли термин.
|
||||
|
||||
Ты **ничего не правишь**. Каждая находка — готовая формулировка на замену,
|
||||
которую зовущий подставит командой (`edit <слаг> --title …`, `edit <слаг>
|
||||
--why …`) или впишет редактором. Файлы ты только читаешь.
|
||||
|
||||
## Что тебе дают
|
||||
|
||||
Список записей или каталог задач: файлы `items/<slug>.md`, а с ними — индексы
|
||||
(`BACKLOG.md`, `ROADMAP.md`, `REJECTED.md`), где та же запись представлена
|
||||
строкой. **Строка индекса вычитывается наравне с файлом**: по ней запись
|
||||
выбирают, не открывая тела, и «зачем» в ней повторяется дословно.
|
||||
|
||||
Плюс, если зовущий их назвал, документы проекта — паспорт, архитектура,
|
||||
конвенции: по ним проверяется, известен ли термин. **Не назвали — считай
|
||||
известными только те слова, что встречаются в других поданных записях**, и
|
||||
говори об этом в границах покрытия.
|
||||
|
||||
## Правила
|
||||
|
||||
Дом — `shared/language.md` в репозитории плагинов, и там же объяснено, зачем
|
||||
стиль вообще нужен. Здесь только то, что нужно тебе для работы.
|
||||
|
||||
<!-- копия: язык-правила из shared/language.md -->
|
||||
|
||||
У каждого правила названа причина: она же говорит, где правило **не**
|
||||
применяется.
|
||||
|
||||
1. **Глагол вместо отглагольного существительного, активный залог.** «Обработчик
|
||||
не проверяет владельца», а не «проверка владельца не осуществляется»;
|
||||
«скрипт переписывает индекс», а не «индекс переписывается скриптом».
|
||||
Отглагольное существительное прячет того, кто действует, — а в техническом
|
||||
тексте важен именно он. Страдательный залог **остаётся**, когда деятель
|
||||
неизвестен или неважен: «файл удаляется» верно, если удаляет любая из трёх
|
||||
команд.
|
||||
|
||||
2. **Факт вместо оценки.** «Время ответа доходит до 800 мс», а не «работает
|
||||
медленно»; «тело 40 МиБ держит блокировку 5 секунд», а не «большие тела
|
||||
тормозят». Оценка допустима, когда факт стоит рядом, в той же фразе. Без
|
||||
факта это настроение, а не сведение, — и находка тем ценнее, что оценку
|
||||
потом не проверить.
|
||||
|
||||
3. **Стоп-слова.** Канцелярит (является, осуществляется, в целях, в рамках,
|
||||
данный, вышеуказанный), вводные-паразиты (в общем, как известно, стоит
|
||||
отметить), усилители (очень, крайне, достаточно, абсолютно, максимально),
|
||||
синонимы одного качества («понятный и простой»), неопределённое
|
||||
(соответствующий, определённый, некоторый).
|
||||
|
||||
Проверка одна: **вычеркни слово — смысл изменился, оставляй.** И осторожно с
|
||||
вводными: «если», «иначе», «при таком-то условии», «в отличие от» несут
|
||||
условие и противопоставление, то есть сведения, — их не трогают.
|
||||
|
||||
4. **Одна мысль — одно предложение.** Предложение с двумя независимыми
|
||||
утверждениями делится. **Причинную связь не режут**: «поэтому», «иначе», «раз
|
||||
так» — смысл, а не длина; рубленые фразы ради краткости тут вредят.
|
||||
|
||||
**Поля меты не делятся.** «Зачем» в мете задачи по формату — одно
|
||||
предложение: оно повторяется строкой индекса, и второму там не поместиться.
|
||||
Тесно — сокращают, но не делят. То же с любым полем вида `- **Имя:** …`.
|
||||
|
||||
5. **Англицизм, у которого есть живое русское слово, заменяется.**
|
||||
|
||||
| Калька | Русский аналог |
|
||||
| --- | --- |
|
||||
| флоу | поток, процесс, сценарий |
|
||||
| фикс, зафиксить | исправление, исправить, починить |
|
||||
| чекать | проверять |
|
||||
| апрув, заапрувить | согласование, согласовать |
|
||||
| best-effort | по возможности |
|
||||
| кейс | случай, сценарий |
|
||||
| перформанс | производительность |
|
||||
| матчинг, смэтчить | сопоставление, сопоставить |
|
||||
| зарелизить | выпустить, выложить |
|
||||
| отрефакторить | переписать, разделить, убрать второй путь |
|
||||
|
||||
Насильно не переводится то, что является **именем вещи**: термины технологий
|
||||
и протоколов (`SQL`, `API`, `CSV`, `N+1`, `IDOR`), имена классов, методов,
|
||||
полей, таблиц и команд, слаг, а также термин, у которого нет точного русского
|
||||
эквивалента и который в команде уже прижился.
|
||||
|
||||
Цель — простой и точный текст, а не пуризм. Русский аналог звучит коряво или
|
||||
искажает смысл — остаётся термин.
|
||||
|
||||
6. **Слово из своего словаря не трогается — список закрыт.** Оговорка «термин
|
||||
прижился» без списка проверяема на глаз и потому не проверяема: прижившимся
|
||||
выглядит любое слово, встреченное трижды.
|
||||
|
||||
| Термин | Что называет |
|
||||
| --- | --- |
|
||||
| интейк | заведение записи с фильтром и дедупом: «заведение» называет создание файла, слить их — смешать две операции |
|
||||
| триаж | стадия конвейера, сводящая находки в решение |
|
||||
| провенанс | обязательное свойство числа: чем и при каких условиях получено. «Источник» рядом называет саму запись, а не свойство |
|
||||
| дедуп, дедупликация | сверка нового против уже лежащего |
|
||||
| чек-лист | перечень, по которому идут сверху вниз, называя исход каждой строки |
|
||||
| дифф, `--base` | разница между состояниями в git |
|
||||
| промпт | текст, которым зовут модель |
|
||||
| change, capability, spec | сущности OpenSpec, имена вещей чужого инструмента |
|
||||
| generative, applicative | роды проходов ревью, вводятся определением по месту |
|
||||
|
||||
**Список закрыт.** Слово не отсюда и не из таблицы имён вещей выше — находка,
|
||||
а не «принятый стиль»: у него либо есть живой русский аналог, либо оно
|
||||
требует ввода одной строкой при первом употреблении.
|
||||
|
||||
Отсюда же читается снятое. Эти слова из текстов убраны, и возвращать их не
|
||||
надо: **конфляция** (смешение), **декорреляция** (разведённость, разведён с
|
||||
кем-то), **непоймание** (почему не поймали), **эвал-сет** (проверочный
|
||||
набор), **гайд** (руководство). Каждое было латинизмом или калькой при живом
|
||||
русском слове, и каждое к моменту снятия жило в трёх-шести файлах разом — то
|
||||
есть выглядело словарём, не будучи им.
|
||||
|
||||
7. **Жаргон и метафоры заменяются прямым называнием.** Автору образ понятен,
|
||||
читателю — нет.
|
||||
|
||||
| Метафора-жаргон | Прямо |
|
||||
| --- | --- |
|
||||
| рычаг (кэша, отбора) | условие отбора, параметр |
|
||||
| навешен не на тот счётчик | завязан не на тот счётчик |
|
||||
| переширокий матчинг по имени | слишком грубое сопоставление по имени, слишком много слабых совпадений |
|
||||
| костыль | временное решение, обходной путь — и в чём именно |
|
||||
| просело, отвалилось | стало медленнее на столько-то, перестало отвечать |
|
||||
|
||||
Проверка: **фраза требует, чтобы читатель додумал образ, — заменяется
|
||||
буквальным описанием того, что происходит.**
|
||||
|
||||
8. **Термин, которого нет в документах проекта, вводится одной строкой или не
|
||||
употребляется.** Термин, не встречающийся ни в паспорте, ни в архитектуре, ни
|
||||
в конвенциях, — свой словарь у отдельной записи, а это самый дешёвый способ
|
||||
сделать беклог нечитаемым для того, кто вернётся к нему через квартал.
|
||||
Заменять незнакомый термин догадкой нельзя: догадка о предметной области
|
||||
дороже непонятного слова, потому что выглядит понятной.
|
||||
|
||||
**Слово, занятое в другом смысле, — то же нарушение.** Термин, который в
|
||||
одном документе проекта значит одно, а здесь другое, ломает оба.
|
||||
|
||||
9. **Имя файла — английское слово по сути, а не транслит.** `queue-as-table`, а
|
||||
не `ochered-tablicej`; `move-parse-strict`, а не `razbor-hoda`. Транслит
|
||||
нечитаем тому, кто ищет по смыслу, и не сокращается, а имя стоит в ссылках,
|
||||
коммитах и путях, которые набирают руками. Переименование — **перенос ссылок
|
||||
одним проходом**, а не правка одного файла.
|
||||
|
||||
<!-- /копия: язык-правила -->
|
||||
|
||||
### Что из этих правил докладывается особым образом
|
||||
|
||||
**Правило 4, поля меты.** «Зачем» по формату — одно предложение, потому что
|
||||
повторяется строкой индекса. Предложить разбить его надвое — находка **против**
|
||||
формата, а не по нему; тесно — предлагай сокращение.
|
||||
|
||||
**Правило 8, неизвестный термин.** Своей догадки не подставляй — ты не знаешь
|
||||
предметную область. Пиши «термин «X» не встречается ни в документах, ни в других
|
||||
поданных записях — введи строкой или назови известным словом». Свой словарь у
|
||||
отдельной задачи — самый дешёвый способ сделать беклог нечитаемым тому, кто
|
||||
вернётся к нему через квартал.
|
||||
|
||||
**Правило 9, имя файла.** Кириллицу в слаге и не-kebab-case ловит `tasks.py` —
|
||||
про них молчи. Твоё — **транслит**, потому что машина проверяет его эвристикой и
|
||||
ловит не всё: `sostoyanie-partii` проходит мимо неё. Находка — готовый
|
||||
английский слаг на замену плюс напоминание, что переименование это перенос
|
||||
ссылок одним проходом, а не правка одного файла.
|
||||
|
||||
## Чего ты не проверяешь
|
||||
|
||||
Не своё бывает двух разных родов, и поступают с ними по-разному.
|
||||
|
||||
**Чужому подрядчику — строкой в границах покрытия.** Форма записи у `task-form`;
|
||||
язык документов проекта у `doc-wording`; их согласованность между собой у
|
||||
`doc-consistency`, соответствие коду у `doc-code-drift` — до записей эти двое не
|
||||
доходят вовсе, но если ты открыл документ как словарь и увидел расхождение в нём
|
||||
самом, оно их. Увидел — назови в конце одной строкой, чтобы находка не пропала,
|
||||
но находкой не оформляй.
|
||||
|
||||
**Машинной проверке — вообще ничего.** Всё, что ловит `tasks.py check` (состав и
|
||||
написание секций, теги, тег `question` при непустом разделе «Вопросы»,
|
||||
согласованность файлов с индексами, битые ссылки), **не пиши даже строкой**: это
|
||||
не потерянная находка, а уже проверенное. Повторять машинную проверку словами —
|
||||
заводить второй дом для одного правила. Наличие разделов своего типа и число
|
||||
критериев `check` только считает — поимённо их судит `tasks.py ready`, и это
|
||||
тоже не твоя находка: твоя — язык того, что уже написано.
|
||||
|
||||
**Содержание работы**: нужна ли задача, верно ли выбрана цель, не крупна ли она,
|
||||
достаточна ли декомпозиция. Это разбор, и его ведёт человек со скиллом `tasks`.
|
||||
|
||||
**Полезное действие, параллельность и работающий заголовок** — тоже не твои.
|
||||
Они в доктрине языка, судит их человек: находка по ним требует увидеть текст
|
||||
целиком, а не фразу.
|
||||
|
||||
## Порог вмешательства
|
||||
|
||||
<!-- копия: порог-правки из shared/language.md -->
|
||||
|
||||
**Правка без нарушенного правила не делается.** Текст, переписанный «чтобы
|
||||
звучало лучше», обесценивает список замечаний: когда половина из них вкусовая,
|
||||
перестают читать весь список, и вместе с ним пропадают настоящие находки.
|
||||
Сомневаешься — не правь. Формулировка, которая просто **не твоя**, — не находка.
|
||||
|
||||
**Систематичность нарушения — не довод в его пользу.** Одна и та же ошибка в
|
||||
пяти файлах не становится «принятым стилем»: чаще это значит, что правило не
|
||||
применялось вовсе, — и находка тем важнее. «Так сделано везде» годится как
|
||||
основание для **одной находки на весь набор** («правило N нарушено в пяти
|
||||
записях, перечень: …»), но не как основание промолчать. Принятым считается
|
||||
только то, что назвал зовущий или что записано в конвенциях проекта.
|
||||
|
||||
<!-- /копия: порог-правки -->
|
||||
|
||||
Одна запись может дать несколько находок, но каждое место правится один раз: не
|
||||
предлагай два варианта на выбор, предлагай лучший.
|
||||
|
||||
## Доклад
|
||||
|
||||
<!-- копия: вычитка-доклад из shared/language.md -->
|
||||
|
||||
Находки по одной, в порядке важности: залог и оценки → жаргон и англицизмы →
|
||||
стоп-слова. Первые меняют, **что** читатель понимает; последние — только сколько
|
||||
он на это тратит.
|
||||
|
||||
```
|
||||
<файл>
|
||||
правило: <номер и короткое имя>
|
||||
сейчас: <как написано>
|
||||
предложение: <готовая формулировка, подставляемая как есть>
|
||||
почему: <одна фраза>
|
||||
```
|
||||
|
||||
В конце — **границы покрытия**: сколько файлов просмотрено из скольких, какие не
|
||||
смотрел и почему, и по чему проверялись термины (документы проекта названы или
|
||||
нет). Отчёт без этой строки читается как «всё вычитано», не сообщая, какая часть
|
||||
осталась нетронутой. Туда же — строка «замечено не по моей части»; машинно
|
||||
проверяемое в неё **не идёт**.
|
||||
|
||||
Ничего не нашёл — так и скажи одной строкой. Пустой доклад с границами покрытия
|
||||
полезнее выдуманной находки.
|
||||
|
||||
<!-- /копия: вычитка-доклад -->
|
||||
|
||||
**Находку в заголовке или в «зачем» отмечай особо.** По ним запись выбирают, и
|
||||
подставляются они командой, а не редактором: зовущий обязан показать
|
||||
предложенное человеку вместе с тем, что было. Прочие правки в теле применяются
|
||||
сразу.
|
||||
@@ -0,0 +1,247 @@
|
||||
---
|
||||
name: groom
|
||||
description: "Груминг беклога — интерактивный разбор, отвечающий на два вопроса: что сейчас самое важное и что перестало быть важным. Ответ записывается порядком строк в беклоге: первая строка — то, что делают следующим. Разбирает накопившиеся вопросы, переоценивает задачи порциями по 5–8 (сделано попутно, отменено решением, слилось с соседней, подешевело, разрослось, стало сырьём), закрывает отжившее с причиной и расставляет очередь. Использовать, когда просят разобрать беклог, расставить приоритеты, решить «что делать дальше», провести груминг или переоценку, а также когда вернулись к проекту после перерыва и надо понять, где остановились. Формат и содержимое записей — скилл tasks; выполнение задачи — конвейер проекта."
|
||||
---
|
||||
|
||||
# Груминг: что важно, что перестало
|
||||
|
||||
Скилл отвечает на **два вопроса**, и всё, что не служит им, — не его работа:
|
||||
|
||||
1. **Что сейчас самое важное?**
|
||||
2. **Что перестало быть важным?**
|
||||
|
||||
Ответ на оба **записывается порядком строк в беклоге**: первая строка секции —
|
||||
то, что делают следующим; то, что перестало быть важным, из беклога уходит с
|
||||
причиной. Приоритет — свойство очереди, а не задачи, и живёт он в индексе
|
||||
(правило 4 скилла `tasks`). Груминг — единственное место, где очередь
|
||||
назначается человеком.
|
||||
|
||||
**Скилл интерактивный.** Он не «приводит беклог в порядок» сам: суждение о
|
||||
важности принадлежит человеку, и весь ход — это подготовленные развилки с
|
||||
рекомендацией. Что решается фактом (сделано, отменено, дублируется), решается
|
||||
без вопросов и показывается списком.
|
||||
|
||||
Форматом и содержимым записей владеет скилл `tasks` — груминг зовёт его
|
||||
операции, а не правит файлы руками. Выполнением задачи — конвейер проекта.
|
||||
|
||||
## Три правила, из которых всё следует
|
||||
|
||||
1. **Порядок назначает человек, машина его не выводит.** Ни давность, ни тип, ни
|
||||
число задач под целью приоритетом не являются. Единственное место в очереди,
|
||||
назначенное не человеком, — конец секции у сырья, и оно из очереди изъято
|
||||
(`tasks`, правило 4).
|
||||
2. **Порция важнее охвата.** Тридцать задач за заход — это усталость и
|
||||
штамповка: последние десять получат «оставить» не потому, что живы, а потому,
|
||||
что разбор затянулся. Лучше две честные порции, чем один полный проход.
|
||||
3. **Причина уезжает в запись.** Всё, что решено здесь, оставляет след:
|
||||
`--reason` у закрытия и переноса, ответ в теле задачи, строка в докладе.
|
||||
Решение, оставшееся в переписке, будет принято заново через месяц.
|
||||
|
||||
## Когда груминг созрел
|
||||
|
||||
**Зовёт человек.** Скилл сам себя не назначает, но обязан **напоминать**, и
|
||||
признак наблюдаемый, а не календарный:
|
||||
|
||||
- в беклоге появились записи, которых человек ещё не видел (заведены интейком по
|
||||
ходу работы, урожаем ревью, разбором находок);
|
||||
- на верхних строках очереди есть задача с открытым вопросом — очередь
|
||||
показывает то, что взять нельзя;
|
||||
- `tasks.py check` печатает «готово к взятию: 0 из N» — брать сегодня нечего.
|
||||
|
||||
Порога в неделях нет намеренно: счётчик простоя пришлось бы вести руками, а
|
||||
решает всё равно человек. Признак — **очередь перестала быть твоей**: взялся
|
||||
перечитывать, почему эти задачи стоят в таком порядке, — пора.
|
||||
|
||||
## Вопрос, блокер, необратимое
|
||||
|
||||
| | Что это | Когда спрашиваем | Что останавливает |
|
||||
| --- | --- | --- | --- |
|
||||
| **Вопрос** | решение человека | на груминге, пачкой | взятие задачи в работу |
|
||||
| **Блокер** | работа не может продолжаться ни одной задачей | немедленно | всё |
|
||||
|
||||
Право на **необратимое** — третье и отдельное: что именно необратимо, называет
|
||||
`CLAUDE.md` проекта, и спрашивается оно всегда, независимо от того, когда был
|
||||
последний груминг.
|
||||
|
||||
**Блокер определяется исходом, а не одновременностью.** Встали разом или
|
||||
задачи выпадали по одной — если продолжать нечем, это блокер, и человек
|
||||
спрашивается немедленно, а не ждёт ближайшего груминга.
|
||||
|
||||
**Отличать вопрос от застревания.** Правило про остаток принадлежит управлению
|
||||
задачами: оно решает, **сделана задача или вышла**, а это исход планирования, не
|
||||
исполнения. **Ниже канонический текст; конвейер проекта на него ссылается, а не
|
||||
пересказывает** — два экземпляра одного правила разъезжаются, и разъезжаются
|
||||
незаметно, потому что расхождение видно только на редком входе.
|
||||
|
||||
> Есть остаток, который доводится без ответа, — задача продолжается, вопрос
|
||||
> записывается в файл. Остатка нет — задача возвращается в беклог.
|
||||
|
||||
С двумя оговорками, без которых тест ошибается:
|
||||
|
||||
> **Остаток, который материализует нерешённое** — записывает в хранилище,
|
||||
> журнал, витрину **или наружу** состояние, зависящее от неотвеченного вопроса,
|
||||
> — **не остаток**. Решение поднимается до начала записи: откатить запись
|
||||
> дороже, чем подождать ответ, а иногда невозможно. «Наружу» — часть правила, а
|
||||
> не пример: выкладка, публикация и отправка данных третьей стороне не
|
||||
> откатываются тем более.
|
||||
|
||||
> **Пол для остатка:** остаток, из которого пропала польза, названная в «зачем», —
|
||||
> это не сделанная задача, а вернувшаяся в беклог.
|
||||
|
||||
## Ход груминга
|
||||
|
||||
Четыре шага, и порядок — зависимость, а не список.
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
check["tasks.py check (+ --fix)<br/>результат — строкой в доклад"]
|
||||
s1["1. Осмотреться<br/>что накопилось, чего человек ещё не видел"]
|
||||
s2["2. Разобрать вопросы<br/>пачкой, не больше трёх за раз"]
|
||||
s3["3. Что перестало быть важным<br/>порциями по 5–8"]
|
||||
s4["4. Что важно сейчас<br/>расставить порядок строк"]
|
||||
|
||||
check --> s1 --> s2 --> s3 --> s4
|
||||
s2 -->|"неотвеченный вопрос → судим о важности вслепую"| s4
|
||||
s3 -->|"без переоценки очередь строится из протухшего"| s4
|
||||
```
|
||||
|
||||
Схема — **сводка**: процедура каждого шага в
|
||||
[references/portions.md](references/portions.md), и при расхождении прав текст.
|
||||
|
||||
**1. Осмотреться.** `tasks.py check` (при дрейфе — `--fix`), затем показать
|
||||
человеку текущую очередь: верхние строки каждой секции и что появилось с
|
||||
прошлого раза. Это половина ответа на «что важно»: очередь, которую не видели,
|
||||
обсуждать бессмысленно.
|
||||
|
||||
**2. Разобрать вопросы.** Вопрос — решение человека, и разбирается он **пачкой**,
|
||||
а не по одному, как только возник: по одному это дёрганье, пачкой это груминг.
|
||||
Вопрос на верхних строках очереди разбирается **вне очереди порции**: иначе
|
||||
правило «задача с открытым вопросом в работу не берётся» создаёт стимул вопрос
|
||||
не записывать, лишь бы не вычеркнуть задачу из ближайшей работы.
|
||||
|
||||
**3. Что перестало быть важным.** Порциями по 5–8. Сперва то, что решается
|
||||
фактом и не требует ничьего суждения (сделано попутно, отменено решением,
|
||||
дублируется, симптомы одного дефекта), затем то, что решает человек (жива ли,
|
||||
та ли цель, задача ли это ещё).
|
||||
|
||||
**4. Что важно сейчас.** Расстановка порядка — `move --after <слаг>` и
|
||||
`move --first`. Разбирается **не весь беклог, а верх очереди**: первые три-пять
|
||||
строк каждой секции. Ниже пятой строки порядок всё равно перестаёт что-либо
|
||||
значить — до них дойдут после следующего груминга, и очередь к тому времени
|
||||
будет другой.
|
||||
|
||||
## Приоритет: как его расставляют
|
||||
|
||||
**Вопрос ставится сравнением, а не оценкой.** «Насколько важна эта задача» не
|
||||
имеет проверяемого ответа; «что из этих двух делают раньше» — имеет. Поэтому
|
||||
очередь строится попарно и сверху: что первое, что после него.
|
||||
|
||||
Доводы, которые принимаются:
|
||||
|
||||
- **что сломано сейчас** — работоспособность обгоняет развитие, и это не правило
|
||||
вкуса: сломанное дорожает само;
|
||||
- **что разблокирует остальное** — задача, после которой можно взять три другие,
|
||||
стоит раньше любой из трёх;
|
||||
- **что дешевеет от того, что сделано** — работа рядом с только что тронутым
|
||||
кодом стоит меньше, чем та же работа через квартал;
|
||||
- **что дорожает от ожидания** — данные копятся, миграция усложняется, внешний
|
||||
срок приближается;
|
||||
- **цель, которую человек назвал следующей.**
|
||||
|
||||
Довод **записывается причиной** (`move --after <слаг> --reason …`). Порядок без
|
||||
причины — это порядок, который на следующем груминге назначат заново с нуля.
|
||||
|
||||
**Цель и приоритет — независимые оси.** Очередь может идти поперёк целей, и это
|
||||
законно: задачи одной цели не обязаны стоять подряд. Но если под целью годами
|
||||
ничего не поднимается наверх — это разговор про цель, а не про очередь, и он
|
||||
идёт на шаге 3.
|
||||
|
||||
## Документы устаревают тем же ходом работы
|
||||
|
||||
Груминг судит **задачи**, а не документы, и агентов канона не зовёт: они
|
||||
принадлежат плагину `av-dev-docs`, и когда их звать — решает он.
|
||||
|
||||
Но повод назвать это здесь есть: беклог и документы протухают от одного и того
|
||||
же — от сделанной работы. Пришёл на груминг и видишь, что с прошлого раза сделан
|
||||
десяток задач, — скажи строкой, что документы стоит сверить
|
||||
(`av-dev-docs:healthcheck`), и иди дальше. Плагина в проекте нет — сверять нечем,
|
||||
и это тоже строка.
|
||||
|
||||
## Интерактив
|
||||
|
||||
- Вопросы — через `AskUserQuestion`, **не больше трёх за раз**. Порция в 5–8
|
||||
задач обычно даёт больше трёх суждений: веди несколько итераций по ≤3, а не по
|
||||
одному вопросу на задачу и не одним перегруженным запросом.
|
||||
- К каждому варианту — **предварительное суждение, рекомендация первым
|
||||
вариантом**: «предлагаю выкинуть, потому что …». Возразить дешевле, чем судить
|
||||
с нуля.
|
||||
- Всё, что решается фактом, решай сам и показывай списком в докладе.
|
||||
- **Останавливайся на границе порции**, даже если «ещё чуть-чуть осталось».
|
||||
Между порциями — промежуточный доклад.
|
||||
|
||||
Примеры итераций, отбор порции, храповик на залежавшихся —
|
||||
[references/portions.md](references/portions.md).
|
||||
|
||||
## Стимулы, которые процесс создаёт
|
||||
|
||||
Правило, которое можно обойти в свою пользу, будет обойдено.
|
||||
|
||||
**Приёмщик и исполнитель совпадают, и это надо назвать вслух.** Задачу закрывает
|
||||
тот же агент, который её и сделал. Груминг приёмкой не занимается — отдельного
|
||||
ритуала у неё нет, — и настоящих опор остаётся две:
|
||||
|
||||
- **независимый отчёт ревью** — артефакт, написанный не исполнителем; при
|
||||
конвейере `av-dev-code` это отчёт триажа в
|
||||
`openspec/changes/archive/<id>/review/` (до архивации — `changes/<id>/review/`);
|
||||
- **`reopen <слаг> --reason`** — закрытие не окончательно. Заметил на груминге,
|
||||
что закрытая задача сделана не тем, чем обещала, — возвращай, это штатная
|
||||
операция, а не скандал. Индексы под git: `git log -p` по беклогу показывает,
|
||||
что и когда закрыто, потому что закрытие коммитится отдельным коммитом учёта.
|
||||
|
||||
Известные обходы:
|
||||
|
||||
- **Не записать вопрос** на задаче, которую хочется поднять наверх очереди.
|
||||
Защита: вопросы верхних строк разбираются вне очереди порции, шагом 2.
|
||||
- **Оставить всё как есть.** Груминг, на котором ничего не сдвинулось и ничего
|
||||
не закрылось, — это не «беклог в порядке», а не проведённый груминг. Защита:
|
||||
задача из верхних строк, которую и этот заход оставляет без изменений, **либо
|
||||
двигается, либо получает записанную причину**, почему её держат.
|
||||
- **Расставить порядок молча**, без доводов: тогда через месяц он неотличим от
|
||||
случайного. Защита: причина у каждого движения и строка доклада.
|
||||
- **Разобрать много и мелко** вместо немногого и важного: тридцать полей гигиены
|
||||
вместо трёх решений о важности. Защита: гигиена — работа скилла `tasks` и
|
||||
побочный продукт здесь; доклад называет **решения**, а не правки.
|
||||
|
||||
## Слоты проекта
|
||||
|
||||
Груминг не знает ни языка, ни сборки, ни CI. Проект дописывает в `CLAUDE.md`:
|
||||
|
||||
1. **Что считается сломанным** — какая красная проверка обгоняет развитие.
|
||||
Не названо — спрашиваем человека, а не решаем сами.
|
||||
2. **Необратимое** — что спрашивается всегда (тот же слот, что у скилла `tasks`;
|
||||
дом один).
|
||||
3. **Ориентир по размеру порции**, если он замерялся. Умолчание — 5–8 задач, и
|
||||
это **ориентир, а не закон**.
|
||||
|
||||
Числа проекта (сколько задач приходит за месяц, каков прирост беклога) — предмет
|
||||
наблюдения человека, а не константы этого скилла.
|
||||
|
||||
## Доклад
|
||||
|
||||
- Что просмотрено: N из M, сколько порций, по какому признаку отобраны.
|
||||
- Вопросы: разобрано N, из них отвечено без человека N, снято тегов N.
|
||||
- **Что перестало быть важным**: удалено как реализованное (со ссылками), ушло
|
||||
без реализации (с причинами), понижено до сырья, слито, сменило тип или цель.
|
||||
- **Что важно сейчас**: верх очереди по каждой секции — слаги в порядке, и по
|
||||
каждому движению довод одной строкой.
|
||||
- **Границы покрытия**: сколько задач не трогали и какие именно секции, теги или
|
||||
цели остались — иначе доклад читается как «беклог разобран».
|
||||
- `tasks.py check` после правок — результат строкой.
|
||||
|
||||
## Чего этот скилл не делает
|
||||
|
||||
Не пишет код и не выполняет задачи. Не заводит и не переоформляет записи сам по
|
||||
себе — формат и содержимое ведёт `tasks` (груминг зовёт его операции). Не решает
|
||||
за человека, что важно: он готовит развилки и рекомендует. Не принимает
|
||||
закрытые задачи отдельным ритуалом — `reopen` есть, момента у него нет. Не судит
|
||||
документы проекта — это плагин `av-dev-docs`.
|
||||
@@ -0,0 +1,163 @@
|
||||
# Порции, разбор и расстановка
|
||||
|
||||
Процедура шагов 2–4 груминга. Рамка и правила — [SKILL.md](../SKILL.md).
|
||||
|
||||
Начинается всё с `tasks.py check` (и `check --fix`, если дрейф накопился) —
|
||||
результат идёт строкой в доклад.
|
||||
|
||||
## Шаг 2. Разбор вопросов
|
||||
|
||||
`tasks.py list --questions` — всё, что накопилось. Порядок по каждому вопросу:
|
||||
|
||||
1. **Проверь, не отвечен ли он уже** — решением, документом, соседним
|
||||
изменением, самим ходом сделанной с тех пор работы. Отвеченный вопрос не
|
||||
выносится человеку: это самая частая находка и она не требует ничьего
|
||||
решения.
|
||||
2. **Сформулируй развилку** с вариантами и последствием каждого, рекомендация —
|
||||
первым вариантом.
|
||||
3. **Вынеси пачкой** через `AskUserQuestion`, не больше трёх за раз.
|
||||
4. **Запиши ответ в тело задачи, опустоши раздел «Вопросы»**, сними тег
|
||||
(`edit <slug> --rm-tag question`), **перепиши «зачем»**: «Решено: …» на вопрос
|
||||
«почему это лежит в беклоге» уже не отвечает. Опустошение раздела — не
|
||||
уборка, а условие взятия: правило и причина в скилле `tasks`,
|
||||
[references/task-format.md](../../tasks/references/task-format.md).
|
||||
|
||||
**Вопросы на верхних строках очереди разбираются вне очереди порции** — здесь
|
||||
же, даже если сама задача в порцию переоценки не попала. Иначе правило «задача с
|
||||
открытым вопросом в работу не берётся» создаёт стимул вопрос не записывать, лишь
|
||||
бы не вычеркнуть задачу из ближайшей работы.
|
||||
|
||||
## Шаг 3. Что перестало быть важным
|
||||
|
||||
Цель — выкинуть то, что перестало быть задачей, и вернуть остальному честное
|
||||
состояние. Не «пересмотреть всё», а «пересмотреть порцию до конца».
|
||||
|
||||
### Порция и правило остановки
|
||||
|
||||
- **5–8 задач за порцию.** Размер обоснован усталостью, а не пропускной
|
||||
способностью, и менять его не надо — **надо брать несколько порций**.
|
||||
- **Отбор порций по порядку:**
|
||||
1. **свежее** — заведённое с прошлого груминга: оно ещё не проходило ни одной
|
||||
проверки на нужность. Свежесть меряется git'ом, как и залежалость, — датой
|
||||
появления файла в истории;
|
||||
2. дальше **по залежалости** — `list --stale`;
|
||||
3. по потребности — одна секция целиком, один тег (партия ревью), одна цель
|
||||
(`--goal`), список от человека.
|
||||
- **Останавливайся на границе порции**, даже если «ещё чуть-чуть осталось».
|
||||
Между порциями — промежуточный доклад.
|
||||
|
||||
### Что делать с каждой задачей
|
||||
|
||||
Сперва то, что не требует ничьего решения:
|
||||
|
||||
1. **Проверь, не сделано ли уже.** Задача, реализованная попутно в соседнем
|
||||
изменении, — самая частая находка. Смотри код, документацию, историю коммитов
|
||||
по ключевым словам. Удаление «как реализованной» деструктивно и без следа
|
||||
(в `REJECTED.md` реализованные не пишутся), поэтому порог улики жёсткий:
|
||||
`close <slug> --implemented` только имея **конкретный коммит или строку
|
||||
документа**, закрывающие задачу, и ссылка идёт в доклад. Есть лишь косвенные
|
||||
признаки — не удаляй сам, вынеси в пачку вопросов. Сделана частично → задача
|
||||
сжимается до остатка: тело правишь редактором, заголовок и «зачем» — через
|
||||
`edit`.
|
||||
2. **Проверь, не отменена ли решением.** Документ, ADR или архивное изменение
|
||||
мог закрыть вопрос иначе — тогда `close <slug> --reason "<ссылка на
|
||||
решение>"`. Задача закрывается не только коммитом.
|
||||
3. **Проверь пересечения.** Две задачи об одном — содержимое в одну, вторую
|
||||
`close <slug> --reason "слита с <другой-слаг>"`. Смотри **шире порции**:
|
||||
интейк дедуплицирует новое против существующего, но никогда не пересматривает
|
||||
уже лежащее, и две задачи с одной причиной могут лежать рядом месяцами.
|
||||
4. **Пере-кластеризуй по общей причине.** Несколько задач, оказавшихся симптомами
|
||||
одного дефекта, сливаются в одну — это находка, которую интейк дать не мог.
|
||||
5. **Гигиена полей** — протухшее «зачем», вопрос в прозе, снятый ответ, свойство
|
||||
репозитория в рамках, предписание процесса в теле, тип, разошедшийся с
|
||||
задачей, границы вместо реализации в разделе «Затрагивает». Список и правила —
|
||||
в скилле `tasks`. **Груминг — то самое место, где беклог добирает тип и
|
||||
разделы его схемы:** требовать их на входе значило бы выгонять в заметки то,
|
||||
что должно лежать задачей, а к взятию в работу они уже обязательны (`ready`).
|
||||
Блок здоровья `check` печатает, сколько записей готово к взятию, — по этому
|
||||
числу и видно, добрал ли груминг.
|
||||
|
||||
Гигиена — **побочный продукт, а не предмет**. Тридцать полей вместо трёх
|
||||
решений о важности означают, что груминг не состоялся.
|
||||
|
||||
Затем — то, что решает человек:
|
||||
|
||||
6. **Жива ли она вообще.** Контекст мог измениться: ушла зависимость, отпал
|
||||
сценарий, обошли иначе. Здесь и звучит вопрос, выкидывать ли.
|
||||
7. **Та ли цель — и нужна ли она вообще.** `feature`, которой не находится цель,
|
||||
— кандидат на выход: новая возможность вне цели это возможность, которой никто
|
||||
не заказывал. Операционной задаче (`fix`, `chore`, `research`) цель не нужна,
|
||||
и выдумывать её здесь не надо.
|
||||
|
||||
**Отменяется и сама цель** — когда замысел оказался неверен, а не когда
|
||||
задача выбрала не ту. Тогда порция расширяется до всех задач этой цели: каждую
|
||||
либо закрыть своей причиной, либо перевесить на другую цель, и только потом
|
||||
закрыть цель. Порядок и почему он такой —
|
||||
[task-goal.md](../../tasks/references/task-goal.md#отменённая-цель--сперва-задачи-потом-цель).
|
||||
8. **Задача ли это по-прежнему.** Не проходит `ready` по существу, а не по
|
||||
недописанным разделам → `edit <slug> --type research` и опустошённый раздел
|
||||
«Вопрос», то есть сырьё; дальше штурм. Разрослась → это несколько задач под
|
||||
той же целью, дальше декомпозиция.
|
||||
9. **Не подешевела ли она.** Сделанная с прошлого раза работа меняет цену
|
||||
**других** задач: рядом с только что тронутым кодом та же работа стоит меньше.
|
||||
Это довод и на шаге 4 — задача, внезапно подешевевшая, поднимается в очереди
|
||||
не потому, что стала важнее, а потому, что окно открыто.
|
||||
|
||||
### Храповик на залежавшихся
|
||||
|
||||
Сильно залежавшаяся задача — сигнал сама по себе: её либо ни разу не собирались
|
||||
делать, либо нечем взять. Измеряй наблюдаемым — датой последней правки из git
|
||||
(`list --stale` ставит такие первыми); счётчик «сколько грумингов пережила»
|
||||
нигде не хранится.
|
||||
|
||||
Задача из верхних строк `--stale`, которую и этот заход оставляет без изменений,
|
||||
**либо двигается (меняет цель, поднимается в очереди, уходит с причиной), либо
|
||||
остаётся с явно записанной причиной**, почему её держим (`move <slug> --reason
|
||||
…` — без `--section` секция берётся текущая). Молчаливое «оставить как есть» на
|
||||
давно неподвижной задаче — это решение не принимать решение; запись причины
|
||||
превращает его в осознанное и не даёт тому же вопросу всплыть на следующем
|
||||
груминге.
|
||||
|
||||
## Шаг 4. Что важно сейчас — расстановка
|
||||
|
||||
Разбирается **верх очереди**, а не весь беклог: первые три-пять строк каждой
|
||||
секции. Ниже пятой строки порядок всё равно перестаёт что-либо значить.
|
||||
|
||||
1. **Покажи текущий верх** — `list --index backlog`, по секциям, в том порядке,
|
||||
в каком строки лежат. Плюс состояние проекта из роадмапа: секция `Готово`
|
||||
отвечает на «где мы», `Запланировано` — на «куда шли».
|
||||
2. **Спрашивай сравнением, а не оценкой.** «Что из этих двух делают раньше»
|
||||
имеет проверяемый ответ, «насколько важна эта задача» — нет. Веди попарно и
|
||||
сверху: что первое, что после него.
|
||||
3. **Двигай командой, с причиной** — `move <slug> --after <другой> --reason …`
|
||||
или `move <slug> --first --reason …`. Довод берётся из перечня в
|
||||
[SKILL.md](../SKILL.md#приоритет-как-его-расставляют): сломано сейчас,
|
||||
разблокирует остальное, дешевеет от сделанного, дорожает от ожидания,
|
||||
названная цель.
|
||||
4. **Проверь верх на готовность** — `tasks.py ready <слаг> …` по первым строкам.
|
||||
Задача, стоящая первой и не проходящая `ready`, — это очередь, которая врёт:
|
||||
взять её нельзя. Либо дописывается здесь же, либо уступает место.
|
||||
|
||||
Пример одной итерации:
|
||||
|
||||
> **Верх секции «Игра», сейчас в таком порядке:**
|
||||
> `board-render-once` · `draw-before-full-board` · `move-parse-strict`
|
||||
>
|
||||
> 1. Что делаем первым?
|
||||
> - `draw-before-full-board` *(рекомендую)* — ничья объявляется на неполном
|
||||
> поле: игра врёт о результате, это сломано сейчас
|
||||
> - `board-render-once` — печать поля дублируется; мешает всякой правке
|
||||
> отрисовки, то есть разблокирует остальное
|
||||
> - оставить как есть
|
||||
> 2. `move-parse-strict` — третьей или выше?
|
||||
> - Оставить третьей *(рекомендую)* — ошибка ввода видна игроку сразу
|
||||
> - Поднять второй: тот же разбор трогает `board-render-once`, окно открыто
|
||||
|
||||
Каждый вариант несёт причину — ту самую, что уедет в `--reason`.
|
||||
|
||||
## Что делать, если разбирать нечего
|
||||
|
||||
Беклог пуст или в нём три задачи и все живые — груминг кончается за минуту, и
|
||||
это законный исход. Скажи строкой: очередь такая-то, сдвигать нечего. Придумывать
|
||||
работу, чтобы груминг «состоялся», — ровно тот ритуал без выгоды, от которого
|
||||
процесс избавлялся.
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: tasks
|
||||
description: Ведение задач и целей как каталога markdown-файлов (одна запись = один файл в items/ + строка в одном из индексов). У каждой записи есть тип (goal, feature, fix, chore, research), и тип решает, каких разделов она требует и что с ней можно делать. Заведение записи из диалога, разбор находок аудита/ревью, декомпозиция на независимо полезные части, штурм сырья, гигиена полей и проверка согласованности индексов. Использовать, когда просят добавить задачу/идею/цель, превратить находки ревью в задачи, разбить задачу, проработать идею, поправить формат или проверить беклог. Ритуал между спринтами — скилл session. Не реализует задачи — этим занимается пайплайн проекта.
|
||||
description: Ведение задач и целей как каталога markdown-файлов (одна запись = один файл в items/ + строка в одном из индексов). У каждой записи есть тип (goal, feature, fix, chore, research), и тип решает, каких разделов она требует и что с ней можно делать. Заведение записи из диалога, разбор находок аудита/ревью, декомпозиция на независимо полезные части, штурм сырья, гигиена полей и проверка согласованности индексов. Использовать, когда просят добавить задачу/идею/цель, превратить находки ревью в задачи, разбить задачу, проработать идею, поправить формат или проверить беклог. Расстановка приоритетов и разбор накопившегося — скилл groom. Не реализует задачи — этим занимается скилл решения задачи.
|
||||
---
|
||||
|
||||
# Задачи
|
||||
@@ -9,9 +9,9 @@ description: Ведение задач и целей как каталога mar
|
||||
строка **ровно в одном** индексе. Скилл владеет **форматом и содержимым**:
|
||||
заводит, редактирует, закрывает, разбирает находки ревью, дробит, штурмует сырьё.
|
||||
|
||||
Чем он **не** владеет: ритуалом между спринтами (разбор вопросов → разбор
|
||||
прошедшего спринта → переоценка → выбор цели и набор) — это скилл `session`; и
|
||||
выполнением задачи — это пайплайн проекта.
|
||||
Чем он **не** владеет: **очередью** — что делать следующим и что перестало быть
|
||||
важным, решает скилл `groom`, а этот скилл лишь даёт ему операции; и выполнением
|
||||
задачи — это конвейер проекта.
|
||||
|
||||
## Шесть правил, из которых всё следует
|
||||
|
||||
@@ -35,47 +35,60 @@ description: Ведение задач и целей как каталога mar
|
||||
Поэтому **«зачем» живёт в мете файла**, а строка индекса его лишь
|
||||
повторяет: пока поле лежало только в индексе, восстановление пропавшей
|
||||
строки теряло его молча и навсегда. Единственное исключение намеренное: **в каком
|
||||
индексе лежит задача, знают индексы** — «в спринте» это свойство спринта, а
|
||||
не файла, поля-состояния нет.
|
||||
индексе лежит запись, знают индексы** — поля-состояния в файле нет. И
|
||||
**порядок строк в беклоге**: приоритет это свойство очереди, а не задачи, и в
|
||||
файле ему места нет (правило 4).
|
||||
3. **Причина переживает запись.** Выкинутая без причины задача вернётся через
|
||||
квартал тем же текстом. Реализованная оставляет след в коммите — выкинутая не
|
||||
оставляет ничего, поэтому у неё есть `REJECTED.md`.
|
||||
4. **Порядка нет, есть цель — но цель есть не у всякой задачи.** Приоритетов,
|
||||
«повысить» и «встать раньше» нет: «что делать дальше» отвечает набор спринта,
|
||||
а между спринтами порядок не нужен никому. Цель обязательна там, где она и
|
||||
есть содержание работы, — у **новой возможности** (`feature`). Починка,
|
||||
техдолг и разведка служат работоспособности, а не направлению, и живут без
|
||||
цели законно; в набор спринта они входят помимо его цели. Придуманная им цель
|
||||
— то же враньё, от которого спасает тип.
|
||||
4. **Приоритет — это порядок строк, а цель есть не у всякой задачи.** Очередь
|
||||
внутри секции беклога значима: **первая строка — то, что делают следующим**.
|
||||
Приоритет назначает человек на груминге, машина его не выводит и не угадывает.
|
||||
|
||||
Единственный порядок, который в беклоге всё-таки есть, **производен от типа**,
|
||||
а не назначен человеком: **сырьё** (`research` без раздела «Вопрос») стоит в
|
||||
конце своей категории. Его не берут, и между берущимся оно каждый раз требует
|
||||
открыть файл, чтобы это понять. Раз порядок выводится, его проверяет машина —
|
||||
и приоритетом он не становится.
|
||||
Прежде здесь стояло «порядка нет, есть цель», и обосновано это было тем, что
|
||||
на «что делать дальше» отвечает **набор спринта**. Набора больше нет, а
|
||||
вопрос остался — и без порядка отвечать на него стало нечем.
|
||||
|
||||
**Дом приоритета — индекс, а не файл.** Это то же исключение из правила 2,
|
||||
что и «в каком индексе лежит запись»: приоритет — свойство очереди. Положи он
|
||||
в файл числом, и два соседних файла смогли бы утверждать одно и то же место,
|
||||
а строка индекса — противоречить обоим.
|
||||
|
||||
Цель обязательна там, где она и есть содержание работы, — у **новой
|
||||
возможности** (`feature`). Починка, техдолг и разведка служат
|
||||
работоспособности, а не направлению, и живут без цели законно. Придуманная им
|
||||
цель — то же враньё, от которого спасает тип. **Цель и приоритет —
|
||||
независимые оси:** очередь может идти поперёк целей, и это законно.
|
||||
|
||||
Одно место в очереди назначено **не человеком, а типом**: **сырьё**
|
||||
(`research` без раздела «Вопрос») стоит в конце своей категории. Его не берут,
|
||||
и между берущимся оно каждый раз требует открыть файл, чтобы это понять. Раз
|
||||
это выводится, проверяет и чинит это машина.
|
||||
5. **Тип решает, что с записью можно делать.** Тип — единственная ось и первое
|
||||
поле меты: от него зависит, какие разделы обязательны в теле, нужна ли цель,
|
||||
берётся ли запись в спринт и в каком индексе живёт её строка. Словарь закрыт;
|
||||
берётся ли запись в работу и в каком индексе живёт её строка. Словарь закрыт;
|
||||
ни один тип не подошёл — значит, в записи их два, и её надо разделить.
|
||||
|
||||
## Раскладка
|
||||
|
||||
Каталог задач — **`docs/tasks`, жёстко**: это часть
|
||||
[канона документов](../canon/references/canon.md), и подгоняется под него
|
||||
проект, а не наоборот.
|
||||
Каталог задач — **`tasks/` в корне репозитория, жёстко.** Он принадлежит этому
|
||||
плагину, а не канону документов: `docs/` ведёт другой плагин (`av-dev-docs`), и
|
||||
проект, поставивший учёт работ без него, каталога `docs/` не имеет вовсе. Внутри
|
||||
`docs/` задачи лежали до версии канона 11; непереехавший проект скрипт
|
||||
по-прежнему находит, но новый заводит только в корне.
|
||||
|
||||
```
|
||||
docs/tasks/
|
||||
tasks/
|
||||
items/ задачи и цели файлами, <slug>.md, слаги английские
|
||||
ROADMAP.md состояние проекта: что уже умеет и чего ещё не умеет
|
||||
BACKLOG.md что можно взять — только задачи, целей здесь нет
|
||||
SPRINT.md текущий спринт: цель (или её отсутствие), набор, дата
|
||||
BACKLOG.md что можно взять — только задачи, целей здесь нет.
|
||||
Порядок строк в секции значим: это очередь
|
||||
REJECTED.md ушедшее БЕЗ реализации, с причиной и датой
|
||||
```
|
||||
|
||||
Правило, снимающее путаницу: **`BACKLOG.md` — то, что берут; `ROADMAP.md` — то,
|
||||
подо что берут.** Цель в спринт взять нельзя, поэтому в списке берущихся ей не
|
||||
место.
|
||||
подо что берут.** Цель в работу взять нельзя — берут её задачи, — поэтому в
|
||||
списке берущихся ей не место.
|
||||
|
||||
**Четыре секции роадмапа, и последняя отвечает на половину вопроса:**
|
||||
|
||||
@@ -97,7 +110,7 @@ docs/tasks/
|
||||
роадмап, названный по-своему, читался бы только своим автором. Категории беклога
|
||||
(`Ядро`, `Инфра`) смысла не несут — это полки домена, и остаются делом проекта.
|
||||
Отсюда и разные имена поля меты: у цели **Секция** (часть роадмапа — состояние
|
||||
очереди), у задачи **Категория** (полка, в которую она вернётся из спринта).
|
||||
очереди), у задачи **Категория** (полка домена, на которой она лежит).
|
||||
|
||||
Отсюда четыре правила, которые проверяет `tasks.py check`: **состав закреплён**
|
||||
(чужая секция — ошибка, а не вольность), **все четыре обязаны быть** (нет
|
||||
@@ -116,24 +129,31 @@ docs/tasks/
|
||||
называла слишком много: роадмап **весь** про разработку, и секция с таким именем
|
||||
не отличалась от остальных ничем.
|
||||
|
||||
**Секции «блокеры» в беклоге нет.** Блокер — это *состояние* (спринт не может
|
||||
**Секции «блокеры» в беклоге нет.** Блокер — это *состояние* (работа не может
|
||||
продолжаться ни одной задачей), а не полка: он живёт ровно до ответа человека, и
|
||||
записи в такой секции не успевают жить. Следы блокера остаются вопросами в
|
||||
файлах задач распущенного спринта. Постоянно пустая секция со старой семантикой
|
||||
файлах задач. Постоянно пустая секция со старой семантикой
|
||||
«разбираются пачками» противоречила бы правилу «блокер эскалируется немедленно»,
|
||||
поэтому `init` её заводить отказывается, а `check` о ней говорит. **Проекту,
|
||||
который переезжает с такой секцией, её надо удалить** — это единственное место,
|
||||
где это сказано.
|
||||
|
||||
**Задача живёт в одном индексе за раз.** Взята в спринт — строка переезжает из
|
||||
`BACKLOG.md` в `SPRINT.md`; вышла — обратно. Файл в `items/` при этом **не
|
||||
двигается**: он и есть запись, индексы лишь показывают, где она числится.
|
||||
**Запись живёт в одном индексе за раз.** Индексов два, и выбирает между ними
|
||||
тип: цель в роадмапе, задача в беклоге. Сменился тип — строка переезжает
|
||||
(`edit --type`). Файл в `items/` при этом **не двигается**: он и есть запись,
|
||||
индексы лишь показывают, где она числится и в каком порядке стоит.
|
||||
|
||||
**Порядок строк в беклоге — приоритет**, и он единственное, чего в файле нет
|
||||
(правило 4). Отсюда следствие для всякой машинной правки индекса:
|
||||
восстановленная или перенесённая строка встаёт **в конец своей секции**, и
|
||||
скрипт об этом говорит. Молчаливая вставка выдала бы машинную позицию за
|
||||
решение человека — а решение это его.
|
||||
|
||||
**У сделанной задачи записи не остаётся** — файл и строка удаляются (`close
|
||||
--implemented`). Ей хватает коммита и документации проекта; вторая запись была
|
||||
бы вторым домом для того же факта. Вопрос «что было в спринте N» отвечается
|
||||
даром: `SPRINT.md` лежит под git, `git log -p docs/tasks/SPRINT.md` отдаёт историю
|
||||
всех наборов без отдельного журнала.
|
||||
бы вторым домом для того же факта. Вопрос «что было сделано и когда» отвечается
|
||||
даром: индексы лежат под git, а закрытие коммитится отдельным коммитом учёта —
|
||||
`git log -p tasks/BACKLOG.md` отдаёт историю без отдельного журнала.
|
||||
|
||||
**У достигнутой цели запись остаётся, и это единственное исключение.** Файл
|
||||
удаляется так же, а строка переезжает в секцию `Готово` с датой. Причина в том,
|
||||
@@ -150,7 +170,6 @@ docs/tasks/
|
||||
stateDiagram-v2
|
||||
state "BACKLOG.md — что берут" as B
|
||||
state "ROADMAP.md — подо что берут" as P
|
||||
state "SPRINT.md — набор спринта" as S
|
||||
state "REJECTED.md — ушла без реализации" as R
|
||||
state "записи нет — реализована" as D
|
||||
state "ROADMAP.md, «умеет» — цель достигнута" as A
|
||||
@@ -159,12 +178,9 @@ stateDiagram-v2
|
||||
[*] --> P: add --type goal
|
||||
B --> P: edit --type goal --section
|
||||
P --> B: edit --type feature|fix|chore|research --section
|
||||
B --> S: sprint take
|
||||
S --> B: sprint drop --reason
|
||||
S --> D: close --implemented
|
||||
B --> D: close --implemented
|
||||
P --> A: close --implemented
|
||||
B --> R: close --reason
|
||||
S --> R: close --reason
|
||||
P --> R: close --reason
|
||||
D --> B: reopen --reason
|
||||
R --> B: reopen --reason
|
||||
@@ -194,7 +210,7 @@ stateDiagram-v2
|
||||
часть кода мы трогаем».
|
||||
|
||||
**Целью не становится работа, которой держат проект.** Состав перечислен
|
||||
[в каноне](../canon/references/canon.md), раздел «Сопровождение и эксплуатация»;
|
||||
[в словаре сопровождения](references/operations.md);
|
||||
на вопрос «что приложение будет уметь» ничто из него не отвечает. Им отведена отдельная секция роадмапа,
|
||||
чтобы они были видны в том же экране и при этом не читались как возможности
|
||||
продукта.
|
||||
@@ -206,11 +222,12 @@ stateDiagram-v2
|
||||
секции отвечают на разные вопросы.
|
||||
|
||||
**Сопровождение и эксплуатация — целое и часть**, а не синонимы, и та же тема
|
||||
живёт ещё в двух местах канона: разделе «Эксплуатация» в `architecture.md` и
|
||||
теме ревью `operations`. Словарь у всех трёх общий и живёт одним домом —
|
||||
[canon.md](../canon/references/canon.md), раздел «Сопровождение и эксплуатация».
|
||||
Пересказывать его здесь нельзя: три перечня «чем держат проект» уже разъезжались
|
||||
на «метриках и логах» против «мониторинга».
|
||||
живёт ещё в двух местах: разделе «Эксплуатация» в `architecture.md` и теме ревью
|
||||
`operations`. Словарь у всех трёх общий, дом у него один — `shared/operations.md`
|
||||
в репозитории плагинов, — а здесь лежит дословная копия:
|
||||
[references/operations.md](references/operations.md). Пересказывать его своими
|
||||
словами нельзя: три перечня «чем держат проект» уже разъезжались на «метриках и
|
||||
логах» против «мониторинга».
|
||||
|
||||
Секция выбирается так: очередь значима и обоснована прозой — `Запланировано`;
|
||||
тянется долго и очереди не имеет — `Направления`; не про приложение, а про то,
|
||||
@@ -244,7 +261,7 @@ stateDiagram-v2
|
||||
**поле меты `Тип` первой строкой**; эмодзи в заголовке H1 от него производна, её
|
||||
ставит `add` и чинит `check --fix`.
|
||||
|
||||
| Тип | Обязательные разделы | Цель | В спринт | Устав |
|
||||
| Тип | Обязательные разделы | Цель | В работу | Устав |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 🎯 `goal` | `Завершение` | — | нет | [task-goal.md](references/task-goal.md) |
|
||||
| ✨ `feature` | `Затрагивает`, `Критерии приёмки` | **обязательна** | да | [task-feature.md](references/task-feature.md) |
|
||||
@@ -267,19 +284,19 @@ stateDiagram-v2
|
||||
незаполненности** — «первый, второй или третий вопрос теста готовности не
|
||||
отвечается», — а состояние типом быть не может: оно меняется по мере того, как
|
||||
запись дописывают, а тип меняют командой. Теперь это состояние называется честно:
|
||||
`research` без раздела «Вопрос» — **сырьё**. В спринт не берётся ровно как
|
||||
`research` без раздела «Вопрос» — **сырьё**. В работу не берётся ровно как
|
||||
прежняя идея, лежит в конце своей категории и отбирается `list --raw`.
|
||||
|
||||
Словарь **закрыт**. Открытый разъедется на синонимах — `bug`, `bugfix`, `fix`,
|
||||
`defect`, — и отбор по типу перестанет отвечать на свой единственный вопрос. Ни
|
||||
один тип не подходит — это сигнал, что в задаче их два и её надо разделить.
|
||||
|
||||
**Требуется тип там, где по нему принимают решение:** `sprint take` без типа
|
||||
**Требуется тип там, где по нему принимают решение:** `ready` без типа
|
||||
откажет, потому что не знает, каких разделов требовать. `check` о пропаже только
|
||||
**напоминает** — беклог, заведённый до появления типа, законен, и переоформлять
|
||||
его «заодно» здесь не просят.
|
||||
|
||||
**Тип не выбирает метку ревью и вообще ничего не предписывает пайплайну.**
|
||||
**Тип не выбирает метку ревью и вообще ничего не предписывает конвейеру.**
|
||||
Профиль выбирается по факту изменения, а не по типу задачи: `chore` бывает
|
||||
миграцией схемы, `fix` — правкой публичного контракта. Правило «предписание
|
||||
процесса в теле задачи снимается» типом не отменяется, а подтверждается: он
|
||||
@@ -321,7 +338,7 @@ stateDiagram-v2
|
||||
делать, и какие границы это трогает: эндпоинт или команду, таблицу и миграцию,
|
||||
формат на диске, публичный тип пакета, внешний сервис. Перечень живёт разделом
|
||||
«Затрагивает» (форма — [references/task-format.md](references/task-format.md)) и
|
||||
требуется к взятию в спринт. Без него задача оценивается по объёму текста, а не
|
||||
требуется к взятию в работу. Без него задача оценивается по объёму текста, а не
|
||||
по объёму поверхности, — и оценка систематически занижена ровно там, где текст
|
||||
короткий, а границ много. Названы **границы**, а не то, как они изменятся: план
|
||||
реализации живёт в предложении об изменении, а не в задаче.
|
||||
@@ -329,11 +346,12 @@ stateDiagram-v2
|
||||
**Предметно, но без усложнения.** Текст задачи читает человек, который решает,
|
||||
брать её или нет, и делает это по строке индекса и одному экрану тела.
|
||||
|
||||
Язык — общий для всех проектных текстов, и живёт он одним файлом:
|
||||
[../canon/references/language.md](../canon/references/language.md)
|
||||
(информационный стиль, применённый к задачам и документам канона; там же таблицы
|
||||
англицизмов и жаргона и то, что из стиля отброшено намеренно). Задаче он даёт
|
||||
четыре требования, которые нарушаются чаще прочих:
|
||||
Язык — общий для всех проектных текстов, и дом у него один,
|
||||
`shared/language.md` в репозитории плагинов; здесь лежит дословная копия:
|
||||
[references/language.md](references/language.md) (информационный стиль,
|
||||
применённый к задачам и документам канона; там же таблицы англицизмов и жаргона
|
||||
и то, что из стиля отброшено намеренно). Задаче он даёт четыре требования,
|
||||
которые нарушаются чаще прочих:
|
||||
|
||||
- **глагол вместо отглагольного существительного**: «обработчик не проверяет
|
||||
владельца», а не «проверка владельца не осуществляется»;
|
||||
@@ -356,7 +374,7 @@ stateDiagram-v2
|
||||
## Инструмент (`tasks.py`)
|
||||
|
||||
Пусть `tk="$CLAUDE_PLUGIN_ROOT/skills/tasks/scripts/tasks.py"`, а `D` —
|
||||
`docs/tasks` от корня проекта. `--dir` стоит в примерах намеренно: вызов из
|
||||
`tasks` от корня проекта. `--dir` стоит в примерах намеренно: вызов из
|
||||
подкаталога — обычное дело.
|
||||
|
||||
```
|
||||
@@ -365,11 +383,11 @@ python3 $tk check --dir D --fix # + починить дрейф (тип
|
||||
python3 $tk list --dir D [--stale] [--section S] [--type T] [--tag a,b] [--goal S] [--raw] [--index …] [--questions]
|
||||
python3 $tk add --dir D --slug S --title T --type goal|feature|fix|chore|research [--section S] [--goal G] [--why «зачем»] [--tag a,b]
|
||||
python3 $tk edit S --dir D [--title T] [--why «зачем»] [--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 move S --dir D [--section S] [--reason R] [--after S | --first] # без --section — текущая секция
|
||||
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 | --no-goal) --dir D | take S… | drop S… --reason R | close [--dissolve --reason R]
|
||||
python3 $tk ready S… --dir D # схема типа выполнена — можно брать в работу
|
||||
python3 $tk init --dir D [--sections …] [--items …] [--backlog …] …
|
||||
python3 $tk adopt scan --from … | apply --plan … # разовая адаптация, references/adopt.md
|
||||
```
|
||||
@@ -393,7 +411,7 @@ python3 $tk adopt scan --from … | apply --plan … # разовая адап
|
||||
заголовке ставит скрипт.
|
||||
|
||||
**Мутации правят файл и индексы заодно** — руками строку индекса или мету
|
||||
не пиши, зови `add`/`edit`/`move`/`close`/`sprint`. Смена заголовка, «зачем», типа,
|
||||
не пиши, зови `add`/`edit`/`move`/`close`/`reopen`. Смена заголовка, «зачем», типа,
|
||||
цели и **тегов** — это `edit`: он держит H1 (вместе с эмодзи), мету и индекс
|
||||
согласованными. Снятие тега — `--rm-tag` (после ответа на вопрос снимается
|
||||
`question`), смена цели — `--goal`, типа — `--type`; оба заменяют прежнее
|
||||
@@ -406,7 +424,12 @@ python3 $tk adopt scan --from … | apply --plan … # разовая адап
|
||||
`move` двигает только внутри одного индекса и пишет причину. `--section` у
|
||||
`edit` работает **только** при таком переезде — иначе он отсылает к `move`,
|
||||
потому что смена секции без причины и есть тот дрейф, который потом никто не
|
||||
объяснит. Задача в наборе спринта тип не меняет вовсе: сперва `sprint drop`.
|
||||
объяснит.
|
||||
|
||||
**`move --after <слаг>` и `move --first` — это и есть расстановка приоритета.**
|
||||
Порядок строк в секции значим (правило 4), и двигают его только этой командой:
|
||||
руками поправленная строка не оставляет причины, а причина здесь и есть половина
|
||||
решения.
|
||||
|
||||
Тело задачи скрипт не трогает:
|
||||
`add` кладёт заголовок, мета-блок и шаблон с подсказками, тело дописываешь
|
||||
@@ -435,9 +458,12 @@ python3 $tk adopt scan --from … | apply --plan … # разовая адап
|
||||
где по нему принимают решение. Такие записи идут в `НЕОДНОЗНАЧНО`, и тип им
|
||||
проставляет человек — `edit <слаг> --type …`.
|
||||
|
||||
**Что механизировано, а что нет.** У задачи, взятой в набор (`sprint take` и
|
||||
`check` по задачам спринта), проверяется схема её типа, и у каждой части своя
|
||||
глубина:
|
||||
**Что механизировано, а что нет.** Схему типа проверяет `ready` на входе в
|
||||
работу — там, где по ней принимают решение; `check` поимённо о ней не говорит, а
|
||||
считает: строка здоровья **«схема типа не выполнена: N из M»** называет число и
|
||||
первые слаги, строка **«готово к взятию»** — сколько задач беклога пройдут
|
||||
`ready` целиком (схема плюс цель плюс отсутствие открытого вопроса). Это две
|
||||
разные строки, и совпадение их чисел — совпадение. У каждой части своя глубина:
|
||||
|
||||
- **тип** — жёстко: назван и из закрытого словаря;
|
||||
- **критерии приёмки** (`feature`, `fix`, `chore`) — число пунктов жёстко
|
||||
@@ -514,7 +540,7 @@ python3 $tk adopt scan --from … | apply --plan … # разовая адап
|
||||
**одним проходом вместе с починкой перекрёстных ссылок**, а не по одному слагу.
|
||||
|
||||
Если переводить надо не только задачи, а весь `docs/` — это скилл
|
||||
`av-dev-pm:canon`, и он зовёт этот сценарий сам на своём шаге.
|
||||
`av-dev-docs:canon`, и он зовёт этот сценарий сам на своём шаге.
|
||||
|
||||
### Декомпозиция и штурм сырья
|
||||
|
||||
@@ -536,7 +562,7 @@ python3 $tk adopt scan --from … | apply --plan … # разовая адап
|
||||
| Проход | Что смотрит | Над чем работает |
|
||||
| --- | --- | --- |
|
||||
| `task-form` | заголовок по типу, «зачем» вместо пересказа, границы вместо замысла, годность оракулов, предписание процесса, связь со строкой «Завершения» цели | только `items/`, **открывает файл цели** |
|
||||
| `doc-wording` | залог и отглагольные, оценка без факта, стоп-слова, англицизмы, жаргон, неизвестный термин | любой проектный текст, включая документы канона |
|
||||
| `task-wording` | залог и отглагольные, оценка без факта, стоп-слова, англицизмы, жаргон, неизвестный термин, транслит в слаге | `items/` и строки индексов; документы проекта — только как словарь |
|
||||
|
||||
Разделены они не по охвату, а **по глубине**. Язык проверяется по словам и
|
||||
фразам, поштучно; форма записи требует понять, что задача делает, и открыть
|
||||
@@ -582,7 +608,7 @@ python3 $tk adopt scan --from … | apply --plan … # разовая адап
|
||||
мету файла и строку индекса заодно;
|
||||
- **вопрос, застрявший в прозе** — вынимается в раздел «Вопросы» плюс тег
|
||||
`question` (`edit --add-tag question`), иначе он не виден ни `list
|
||||
--questions`, ни правилу «задача с открытым вопросом в набор не берётся»;
|
||||
--questions`, ни правилу «задача с открытым вопросом в работу не берётся»;
|
||||
- **тег, который некому снять** — `question` после ответа снимается `edit
|
||||
--rm-tag question` вместе с записью ответа в тело **и опустошением раздела
|
||||
«Вопросы»**: судит раздел, а не тег (`references/task-format.md`);
|
||||
@@ -598,7 +624,7 @@ python3 $tk adopt scan --from … | apply --plan … # разовая адап
|
||||
ровно там, где по нему отбирают, **и требует не тех разделов**: у брошенного
|
||||
`fix` останется «Воспроизведение», которого нечем заполнить;
|
||||
- **сырьё, у которого появился вопрос** — разведка обросла формулировкой, но
|
||||
раздел «Вопрос» так и пуст: она числится сырьём и в спринт не берётся.
|
||||
раздел «Вопрос» так и пуст: она числится сырьём и в работу не берётся.
|
||||
Записывается вопрос, и `check --fix` поднимает строку из конца категории;
|
||||
- **границы, названные вместо реализации** — «переписать хранилище на новый
|
||||
драйвер» в разделе «Затрагивает» это не граница, а замысел. Границы —
|
||||
@@ -615,28 +641,35 @@ python3 $tk adopt scan --from … | apply --plan … # разовая адап
|
||||
просто каталог markdown. Текст задач — русский (язык документации проекта);
|
||||
зашита только латиница слага. OpenSpec ему тоже не нужен.
|
||||
|
||||
- **Каталог задач — `docs/tasks`, жёстко**, и `--dir` передаётся явно всегда:
|
||||
- **Каталог задач — `tasks/` в корне, жёстко**, и `--dir` передаётся явно всегда:
|
||||
раскладка канона одинакова во всех проектах, и искать больше нечего. Каталога
|
||||
нет — код 3 и вопрос человеку; `init` заводит его **только** когда проект
|
||||
действительно новый, а перевод чужой раскладки делает `av-dev-pm:canon`.
|
||||
действительно новый, а перевод чужой раскладки делает `av-dev-docs:canon`.
|
||||
У скрипта поиск вверх по дереву ещё жив — он для непереведённых проектов, и
|
||||
полагаться на него скилл не должен: молча найденный чужой каталог это дрейф.
|
||||
- **Настройки живут в `docs/.pm.json`**, ключ `tasks`: **имена** файлов и
|
||||
заголовков, и только если они отличаются от умолчания. Один конфиг на весь
|
||||
канон, а не по одному на каталог. Неизвестный ключ — код 3 на любой команде,
|
||||
так что лишнее слово в этом объекте останавливает работу с задачами целиком.
|
||||
- **Настройки живут в `<каталог задач>/.tasks.json`** — свой файл у своего
|
||||
плагина: **имена** файлов и заголовков, и только если они отличаются от
|
||||
умолчания. Неизвестный ключ — код 3 на любой команде, так что лишнее слово в
|
||||
этом объекте останавливает работу с задачами целиком.
|
||||
|
||||
Дом именно свой, а не `docs/.pm.json`, потому что `docs/` принадлежит плагину
|
||||
канона: проект, поставивший учёт работ без него, каталога `docs/` не имеет
|
||||
вовсе. Прежний ключ `tasks` в `docs/.pm.json` читается, **только когда своего
|
||||
файла нет** — для проектов, заведённых до раскола плагинов; скрипт при этом
|
||||
говорит замечанием, куда его перенести. Есть оба — побеждает свой, и об этом
|
||||
тоже говорится вслух: молча выбранный из двух конфиг это дрейф.
|
||||
- **Секции беклога** берутся из заголовков `##` индекса как есть; их количество
|
||||
и названия — дело проекта (умолчание `Ядро` / `Инфра`). **В конфиге их нет** —
|
||||
второй список разошёлся бы с заголовками молча.
|
||||
|
||||
### Вызов из другого плагина
|
||||
|
||||
`$CLAUDE_PLUGIN_ROOT` раскрывается **только внутри своего плагина**: пайплайн
|
||||
`$CLAUDE_PLUGIN_ROOT` раскрывается **только внутри своего плагина**: конвейер
|
||||
задачи, конвейер ревью и любой другой чужой контекст до `tasks.py` по этой
|
||||
переменной не дотянутся. Мост — **вызов скилла через пространство имён**, а не
|
||||
путь:
|
||||
|
||||
> Чужой контекст зовёт `Skill av-dev-pm:tasks` и называет, что нужно сделать
|
||||
> Чужой контекст зовёт `Skill av-dev-tasks:tasks` и называет, что нужно сделать
|
||||
> («закрой задачу `<слаг>`, реализована»). Скилл разрешает свой
|
||||
> `$CLAUDE_PLUGIN_ROOT` сам. Путь наружу не выносится вовсе.
|
||||
|
||||
@@ -651,8 +684,8 @@ python3 $tk adopt scan --from … | apply --plan … # разовая адап
|
||||
какие в проекте оракулы — семантика гейта в `CLAUDE.md`. Отдельными слотами
|
||||
остаётся то, чего из раскладки не вывести. **Проект дописывает в `CLAUDE.md`**:
|
||||
|
||||
1. **Что такое «сделана»** — чем задача выполняется (пайплайн проекта) и что
|
||||
входит в его определение готовности. Скилл требует лишь **форму**: пайплайн
|
||||
1. **Что такое «сделана»** — чем задача выполняется (конвейер проекта) и что
|
||||
входит в его определение сделанного. Скилл требует лишь **форму**: конвейер
|
||||
проекта пройден + критерии приёмки проверены поимённо.
|
||||
2. **Что считается необратимым** и потому спрашивается у человека всегда
|
||||
(деплой, выкладка наружу, удаление или перезапись данных).
|
||||
@@ -680,6 +713,7 @@ python3 $tk adopt scan --from … | apply --plan … # разовая адап
|
||||
## Чего этот скилл не делает
|
||||
|
||||
Не пишет код, не заводит спеки и предложения об изменении, не берёт задачу в
|
||||
работу — этим занимается пайплайн проекта. Не ведёт спринт и не проводит сессию
|
||||
между спринтами — это `session`. Не решает за пользователя, что важно. Не
|
||||
работу — этим занимается конвейер проекта. **Не ведёт очередь:** что делать
|
||||
следующим и что перестало быть важным — скилл `groom`, а этот даёт ему операции.
|
||||
Не решает за пользователя, что важно. Не
|
||||
переоформляет существующие задачи «заодно»: правится то, чего касается операция.
|
||||
+15
-12
@@ -2,10 +2,10 @@
|
||||
|
||||
Проект, где задачи уже как-то ведутся, и из имеющегося материала **выводится**
|
||||
заполненный каталог задач: цели, задачи, кладбище, индексы. Операция разовая —
|
||||
после неё проект живёт скиллами `tasks` и `session`.
|
||||
после неё проект живёт скиллами `tasks` и `groom`.
|
||||
|
||||
**Это часть приведения проекта к канону.** Раскладку `docs/` целиком ведёт скилл
|
||||
`av-dev-pm:canon`; он же зовёт этот сценарий на шаге «каталог задач», потому что
|
||||
`av-dev-docs:canon`; он же зовёт этот сценарий на шаге «каталог задач», потому что
|
||||
форматом задач владеет `tasks`, а не `canon`. Отдельно сценарий вызывается,
|
||||
когда переводить надо **только** задачи.
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
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 # только чтение
|
||||
--target tasks --out tasks-adopt-plan.json # только чтение
|
||||
python3 $tk adopt apply --plan tasks-adopt-plan.json \
|
||||
--refs docs openspec CLAUDE.md README.md # запись
|
||||
```
|
||||
@@ -63,7 +63,7 @@ python3 $tk adopt apply --plan tasks-adopt-plan.json \
|
||||
## Порядок
|
||||
|
||||
1. **Осмотрись.** Где лежат задачи, роадмап, заметки. Каталог задач по канону —
|
||||
всегда `docs/tasks`. Секции беклога (`--sections`) — по умолчанию
|
||||
всегда `tasks`. Секции беклога (`--sections`) — по умолчанию
|
||||
`Ядро,Инфра`; если у проекта деление другое по существу, оно называется
|
||||
здесь, а не подгоняется под умолчание, и становится **заголовками `##`
|
||||
индекса** — их единственным домом. В `docs/.pm.json` секции не пишутся.
|
||||
@@ -94,22 +94,25 @@ python3 $tk adopt apply --plan tasks-adopt-plan.json \
|
||||
быть названо, иначе следующий агент примет пустой беклог за поломку.
|
||||
|
||||
`apply` печатает состояние по факту: сколько задач без цели (это **ошибки**
|
||||
`check`) и сколько без критериев (`check` их ошибкой не считает, но `sprint
|
||||
take` такую задачу не возьмёт). Закрывается это **порциями переоценки** — шаг 3
|
||||
скилла `session`, 5–8 задач за порцию: проставить цели, превратить «готово,
|
||||
когда» в критерии с оракулами, вынуть вопросы из прозы в раздел «Вопросы».
|
||||
`check`) и сколько не собрало разделы своего типа (для `check` это не ошибка, а
|
||||
строка здоровья, но `ready` такую задачу не пропустит). Закрывается это
|
||||
**порциями груминга** — скилл
|
||||
`groom`, 5–8 задач за порцию: проставить цели, превратить «готово, когда» в
|
||||
критерии с оракулами, вынуть вопросы из прозы в раздел «Вопросы». Там же
|
||||
беклогу впервые назначается **порядок**: после адаптации его нет вовсе, а
|
||||
очередь и есть то, ради чего каталог заводят.
|
||||
|
||||
Готовность к первому спринту — не «`check` зелёный», а «есть 2–5 критериев хотя
|
||||
бы у набора под одну цель».
|
||||
Готовность к первой задаче — не «`check` зелёный», а «`ready` пропускает хотя бы
|
||||
верхние строки очереди».
|
||||
|
||||
## Чего адаптация не делает
|
||||
|
||||
- **Не удаляет источники.** Старый каталог остаётся на месте: сверить и убрать —
|
||||
дело человека, удалять чужое молча нельзя. В доклад идёт готовая команда.
|
||||
- **Не переписывает подписи ссылок.** `[docs/backlog](docs/tasks/BACKLOG.md)` —
|
||||
- **Не переписывает подписи ссылок.** `[docs/backlog](tasks/BACKLOG.md)` —
|
||||
цель поправлена, текст остался; это правится глазами, и таких мест немного.
|
||||
- **Не сочиняет критерии приёмки и не придумывает цели**, которых в материале
|
||||
нет. Придуманная цель хуже отсутствующей: под неё соберут спринт.
|
||||
нет. Придуманная цель хуже отсутствующей: под неё заведут задачи.
|
||||
- **Не трогает историю.** В коммитах старые слаги остаются, и это нормально.
|
||||
|
||||
## Доклад
|
||||
+30
-14
@@ -12,6 +12,12 @@
|
||||
не файл-на-находку.** Если у ревью был триаж — половина работы уже сделана, бери
|
||||
его выход. Если нет — триажируй сам, прежде чем заводить.
|
||||
|
||||
**Штатный отправитель — `av-dev-code:review`** (и `av-dev-code:resolve`, который
|
||||
его вызывает): задач он не заводит сам, а отдаёт отложенные находки **списком
|
||||
урожая** — формулировка, оракул, провенанс — и хранит отчёт триажа вместе с
|
||||
изменением. Приходит и любой другой разбор, вплоть до пересказа человеком; тогда
|
||||
триажа нет и шаг 1 порядка делается руками.
|
||||
|
||||
## Находка агента — не задача
|
||||
|
||||
Мнение агента — **гипотеза, пока у неё нет свидетельства** (падающий тест,
|
||||
@@ -25,7 +31,7 @@
|
||||
переживает запись.
|
||||
- **Находка без свидетельства / низкой уверенности** → **сырьё**: `research`, у
|
||||
которого раздел «Вопрос» и есть недостающее свидетельство («при каких условиях
|
||||
это воспроизводится»). Не `fix`: без `Воспроизведения` его в спринт не
|
||||
это воспроизводится»). Не `fix`: без `Воспроизведения` его в работу не
|
||||
возьмут, и правильно — чинить нечего, пока непонятно, что ломается. Судьба
|
||||
сырья — штурм, где либо найдётся подтверждение, либо оно уедет в
|
||||
`REJECTED.md`.
|
||||
@@ -46,7 +52,7 @@
|
||||
устареть, выноси пользователю, а не заводи молча заново.
|
||||
4. **Разложи по целям — там, где цель нужна.** Большинство находок ревью это
|
||||
`fix` и `chore`, и **цель им не требуется**: они служат работоспособности, а
|
||||
не направлению, и в спринт входят помимо его цели. Придуманная им цель —
|
||||
не направлению. Придуманная им цель —
|
||||
ровно то враньё, от которого спасает тип.
|
||||
|
||||
Цель обязательна у находки, которая оказалась **новой возможностью**
|
||||
@@ -73,21 +79,31 @@
|
||||
Без него через месяц не отличить проверенную находку от догадки.
|
||||
7. `tasks.py check`.
|
||||
|
||||
## Куда девается серьёзность, если приоритетов нет
|
||||
## Куда девается серьёзность находки
|
||||
|
||||
Приоритетов нет, и отображать серьёзность некуда — но **выкидывать её нельзя**.
|
||||
Правило замены:
|
||||
Уровня серьёзности в записи нет — но **выкидывать её нельзя**: серьёзность
|
||||
отображается **в позицию в очереди**, потому что приоритет и есть порядок строк
|
||||
в беклоге (правило 4 [SKILL.md](../SKILL.md)). Отображается через довод, а не
|
||||
напрямую: своей шкалы у интейка нет, доводы расстановки перечислены в
|
||||
[скилле груминга](../../groom/SKILL.md#приоритет-как-его-расставляют), и
|
||||
серьёзность попадает ровно в один из них.
|
||||
|
||||
- **тяжёлая находка со свидетельством** → задача под ту цель, которой она
|
||||
угрожает, и **кандидат в ближайший набор**: серьёзность здесь превращается в
|
||||
довод при выборе цели следующего спринта, а не в уровень в файле. Довод
|
||||
записывается причиной в мете (`--reason`), иначе к моменту набора его
|
||||
никто не вспомнит;
|
||||
- **находка, ломающая уже идущий спринт**, — не интейк вовсе: см. правило
|
||||
вторжения в скилле `session`. В беклог она падает, только если врываться не
|
||||
положено;
|
||||
- **тяжёлая находка со свидетельством о сломанном сейчас** → задача под ту цель,
|
||||
которой она угрожает, и **первой строкой секции**: `move <слаг> --first
|
||||
--reason «сломано сейчас: …»`. Это довод «что сломано сейчас» из перечня
|
||||
груминга — единственный, который не требует сравнения с соседями по очереди,
|
||||
потому что сломанное дорожает само. Позицию всё равно назначает человек, и
|
||||
здесь он её уже назначил: верх очереди для такой находки предъявляется картой
|
||||
шага 5, а не проставляется молча;
|
||||
- **тяжёлая находка о риске, а не о поломке** (дорожает от ожидания,
|
||||
разблокирует остальное) → в конец секции, а довод — причиной в мете
|
||||
(`--reason`). Позицию назначит человек на ближайшем груминге, сравнив её с
|
||||
верхом очереди; без записанного довода сравнивать он будет с нуля;
|
||||
- **находка, которая не ждёт груминга вовсе** (необратимый ущерб, покраснела
|
||||
проверка, которую проект назвал сломанным), — не интейк: это работа прямо
|
||||
сейчас, а в беклог она падает, только если ждать всё-таки можно;
|
||||
- **низкая уверенность или нет свидетельства** → сырьё (`research` с пустым
|
||||
разделом «Вопрос»);
|
||||
разделом «Вопрос»): его место в очереди производно от типа — конец секции;
|
||||
- **мелочь** → строка в пакетный файл;
|
||||
- **уже починено / развилка решена сейчас** → ничего.
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
# Язык проектных текстов
|
||||
|
||||
**Копия.** Дом — `shared/language.md` в репозитории плагинов; язык общий для
|
||||
документов канона и для задач, и потому не принадлежит ни одному плагину.
|
||||
Правится дом, а не этот файл: расхождение ловит `copies.py` на гейте коммита.
|
||||
|
||||
<!-- копия: язык-доктрина из shared/language.md -->
|
||||
|
||||
Правила — для всего, что пишется словами: задачи и цели, документы канона,
|
||||
решения ADR, записки разведки, сообщения коммитов. Не для кода и не для
|
||||
сообщений программы пользователю — там свои конвенции проекта.
|
||||
|
||||
Основа — **информационный стиль** Максима Ильяхова ([учебник
|
||||
бюро](https://bureau.ru/projects/book-text/), книга «Пиши, сокращай»). Он
|
||||
написан для рекламы, статей и писем, поэтому взят не целиком.
|
||||
|
||||
## Зачем он здесь
|
||||
|
||||
Проектный текст читают в двух положениях, и оба неудобные: **выбирают, брать ли
|
||||
задачу**, глядя в строку индекса и один экран тела; и **возвращаются через
|
||||
квартал**, не помня контекста. Оба положения наказывают одно и то же — слова, не
|
||||
несущие сведений. Информационный стиль ровно про это, и его польза здесь не
|
||||
эстетическая: текст, из которого нельзя достать факт, заставляет открывать код,
|
||||
а это и есть цена, которой мы избегаем.
|
||||
|
||||
## Что взято сверх правил вычитки
|
||||
|
||||
Эти три требования судит человек, а не проход вычитки: находка по ним требует
|
||||
увидеть текст целиком, а не фразу.
|
||||
|
||||
**Полезное действие.** У каждого текста есть вопрос, на который он отвечает, и
|
||||
читатель, который этот вопрос задаёт. Не отвечает — не пишется. У задачи это
|
||||
«зачем она нужна» и «что станет наблюдаемо иначе»; у документа канона — его
|
||||
собственный вопрос («что это за система», «как сложено», «почему так решили»).
|
||||
Текст, который не отвечает ни на чей вопрос, сокращается до нуля — это законный
|
||||
исход правки.
|
||||
|
||||
**Параллельность.** Однородное пишется одинаково: пункты списка — одной
|
||||
грамматической формой, разделы одного вида — одним порядком, заголовки одного
|
||||
уровня — одним типом фразы. Разнобой читатель принимает за разницу по существу и
|
||||
ищет её.
|
||||
|
||||
**Заголовок работает.** Заголовок называет содержание раздела, а не тему
|
||||
вообще: «Что проверяет `check`», а не «О проверках». Заголовков ставится
|
||||
столько, чтобы длинный текст можно было просматривать, а не только читать
|
||||
подряд.
|
||||
|
||||
## Что отброшено намеренно
|
||||
|
||||
Инфостиль написан для текстов, где читателя надо удержать. Проектный текст
|
||||
читают потому, что надо, и держать его нечем. Отсюда три расхождения:
|
||||
|
||||
- **Парцелляция и рубленые фразы — нет.** Приём «Коротко. Ещё короче. Вот так»
|
||||
ломает причинную связь, а в решении и в задаче ценность именно в ней:
|
||||
«поэтому», «иначе», «раз так» несут смысл и остаются.
|
||||
- **Не всякое вводное — мусор.** «Если», «иначе», «при таком-то условии»,
|
||||
«в отличие от» — это условия и противопоставления, то есть сведения. Режутся
|
||||
вводные, которые не меняют смысл предложения.
|
||||
- **Скобки и точка с запятой остаются.** В технической записи скобки несут
|
||||
уточнение — имя команды, единицы, слаг, — и запрет на них удлинил бы текст, а
|
||||
не сократил. Запрет на многоточие принимаем: в проектном тексте оно значит
|
||||
«дописать позже», и такой текст лучше не публиковать.
|
||||
|
||||
И общее: инфостиль призывает «снять корону с себя и надеть на читателя». Здесь
|
||||
читатель — **ты сам через квартал** и тот, кто возьмёт задачу. Писать для них
|
||||
значит называть состояние и остаток, а не пересказывать, как было интересно
|
||||
разбираться.
|
||||
|
||||
<!-- /копия: язык-доктрина -->
|
||||
|
||||
## Правила
|
||||
|
||||
<!-- копия: язык-правила из shared/language.md -->
|
||||
|
||||
У каждого правила названа причина: она же говорит, где правило **не**
|
||||
применяется.
|
||||
|
||||
1. **Глагол вместо отглагольного существительного, активный залог.** «Обработчик
|
||||
не проверяет владельца», а не «проверка владельца не осуществляется»;
|
||||
«скрипт переписывает индекс», а не «индекс переписывается скриптом».
|
||||
Отглагольное существительное прячет того, кто действует, — а в техническом
|
||||
тексте важен именно он. Страдательный залог **остаётся**, когда деятель
|
||||
неизвестен или неважен: «файл удаляется» верно, если удаляет любая из трёх
|
||||
команд.
|
||||
|
||||
2. **Факт вместо оценки.** «Время ответа доходит до 800 мс», а не «работает
|
||||
медленно»; «тело 40 МиБ держит блокировку 5 секунд», а не «большие тела
|
||||
тормозят». Оценка допустима, когда факт стоит рядом, в той же фразе. Без
|
||||
факта это настроение, а не сведение, — и находка тем ценнее, что оценку
|
||||
потом не проверить.
|
||||
|
||||
3. **Стоп-слова.** Канцелярит (является, осуществляется, в целях, в рамках,
|
||||
данный, вышеуказанный), вводные-паразиты (в общем, как известно, стоит
|
||||
отметить), усилители (очень, крайне, достаточно, абсолютно, максимально),
|
||||
синонимы одного качества («понятный и простой»), неопределённое
|
||||
(соответствующий, определённый, некоторый).
|
||||
|
||||
Проверка одна: **вычеркни слово — смысл изменился, оставляй.** И осторожно с
|
||||
вводными: «если», «иначе», «при таком-то условии», «в отличие от» несут
|
||||
условие и противопоставление, то есть сведения, — их не трогают.
|
||||
|
||||
4. **Одна мысль — одно предложение.** Предложение с двумя независимыми
|
||||
утверждениями делится. **Причинную связь не режут**: «поэтому», «иначе», «раз
|
||||
так» — смысл, а не длина; рубленые фразы ради краткости тут вредят.
|
||||
|
||||
**Поля меты не делятся.** «Зачем» в мете задачи по формату — одно
|
||||
предложение: оно повторяется строкой индекса, и второму там не поместиться.
|
||||
Тесно — сокращают, но не делят. То же с любым полем вида `- **Имя:** …`.
|
||||
|
||||
5. **Англицизм, у которого есть живое русское слово, заменяется.**
|
||||
|
||||
| Калька | Русский аналог |
|
||||
| --- | --- |
|
||||
| флоу | поток, процесс, сценарий |
|
||||
| фикс, зафиксить | исправление, исправить, починить |
|
||||
| чекать | проверять |
|
||||
| апрув, заапрувить | согласование, согласовать |
|
||||
| best-effort | по возможности |
|
||||
| кейс | случай, сценарий |
|
||||
| перформанс | производительность |
|
||||
| матчинг, смэтчить | сопоставление, сопоставить |
|
||||
| зарелизить | выпустить, выложить |
|
||||
| отрефакторить | переписать, разделить, убрать второй путь |
|
||||
|
||||
Насильно не переводится то, что является **именем вещи**: термины технологий
|
||||
и протоколов (`SQL`, `API`, `CSV`, `N+1`, `IDOR`), имена классов, методов,
|
||||
полей, таблиц и команд, слаг, а также термин, у которого нет точного русского
|
||||
эквивалента и который в команде уже прижился.
|
||||
|
||||
Цель — простой и точный текст, а не пуризм. Русский аналог звучит коряво или
|
||||
искажает смысл — остаётся термин.
|
||||
|
||||
6. **Слово из своего словаря не трогается — список закрыт.** Оговорка «термин
|
||||
прижился» без списка проверяема на глаз и потому не проверяема: прижившимся
|
||||
выглядит любое слово, встреченное трижды.
|
||||
|
||||
| Термин | Что называет |
|
||||
| --- | --- |
|
||||
| интейк | заведение записи с фильтром и дедупом: «заведение» называет создание файла, слить их — смешать две операции |
|
||||
| триаж | стадия конвейера, сводящая находки в решение |
|
||||
| провенанс | обязательное свойство числа: чем и при каких условиях получено. «Источник» рядом называет саму запись, а не свойство |
|
||||
| дедуп, дедупликация | сверка нового против уже лежащего |
|
||||
| чек-лист | перечень, по которому идут сверху вниз, называя исход каждой строки |
|
||||
| дифф, `--base` | разница между состояниями в git |
|
||||
| промпт | текст, которым зовут модель |
|
||||
| change, capability, spec | сущности OpenSpec, имена вещей чужого инструмента |
|
||||
| generative, applicative | роды проходов ревью, вводятся определением по месту |
|
||||
|
||||
**Список закрыт.** Слово не отсюда и не из таблицы имён вещей выше — находка,
|
||||
а не «принятый стиль»: у него либо есть живой русский аналог, либо оно
|
||||
требует ввода одной строкой при первом употреблении.
|
||||
|
||||
Отсюда же читается снятое. Эти слова из текстов убраны, и возвращать их не
|
||||
надо: **конфляция** (смешение), **декорреляция** (разведённость, разведён с
|
||||
кем-то), **непоймание** (почему не поймали), **эвал-сет** (проверочный
|
||||
набор), **гайд** (руководство). Каждое было латинизмом или калькой при живом
|
||||
русском слове, и каждое к моменту снятия жило в трёх-шести файлах разом — то
|
||||
есть выглядело словарём, не будучи им.
|
||||
|
||||
7. **Жаргон и метафоры заменяются прямым называнием.** Автору образ понятен,
|
||||
читателю — нет.
|
||||
|
||||
| Метафора-жаргон | Прямо |
|
||||
| --- | --- |
|
||||
| рычаг (кэша, отбора) | условие отбора, параметр |
|
||||
| навешен не на тот счётчик | завязан не на тот счётчик |
|
||||
| переширокий матчинг по имени | слишком грубое сопоставление по имени, слишком много слабых совпадений |
|
||||
| костыль | временное решение, обходной путь — и в чём именно |
|
||||
| просело, отвалилось | стало медленнее на столько-то, перестало отвечать |
|
||||
|
||||
Проверка: **фраза требует, чтобы читатель додумал образ, — заменяется
|
||||
буквальным описанием того, что происходит.**
|
||||
|
||||
8. **Термин, которого нет в документах проекта, вводится одной строкой или не
|
||||
употребляется.** Термин, не встречающийся ни в паспорте, ни в архитектуре, ни
|
||||
в конвенциях, — свой словарь у отдельной записи, а это самый дешёвый способ
|
||||
сделать беклог нечитаемым для того, кто вернётся к нему через квартал.
|
||||
Заменять незнакомый термин догадкой нельзя: догадка о предметной области
|
||||
дороже непонятного слова, потому что выглядит понятной.
|
||||
|
||||
**Слово, занятое в другом смысле, — то же нарушение.** Термин, который в
|
||||
одном документе проекта значит одно, а здесь другое, ломает оба.
|
||||
|
||||
9. **Имя файла — английское слово по сути, а не транслит.** `queue-as-table`, а
|
||||
не `ochered-tablicej`; `move-parse-strict`, а не `razbor-hoda`. Транслит
|
||||
нечитаем тому, кто ищет по смыслу, и не сокращается, а имя стоит в ссылках,
|
||||
коммитах и путях, которые набирают руками. Переименование — **перенос ссылок
|
||||
одним проходом**, а не правка одного файла.
|
||||
|
||||
<!-- /копия: язык-правила -->
|
||||
|
||||
## Порог правки
|
||||
|
||||
<!-- копия: порог-правки из shared/language.md -->
|
||||
|
||||
**Правка без нарушенного правила не делается.** Текст, переписанный «чтобы
|
||||
звучало лучше», обесценивает список замечаний: когда половина из них вкусовая,
|
||||
перестают читать весь список, и вместе с ним пропадают настоящие находки.
|
||||
Сомневаешься — не правь. Формулировка, которая просто **не твоя**, — не находка.
|
||||
|
||||
**Систематичность нарушения — не довод в его пользу.** Одна и та же ошибка в
|
||||
пяти файлах не становится «принятым стилем»: чаще это значит, что правило не
|
||||
применялось вовсе, — и находка тем важнее. «Так сделано везде» годится как
|
||||
основание для **одной находки на весь набор** («правило N нарушено в пяти
|
||||
записях, перечень: …»), но не как основание промолчать. Принятым считается
|
||||
только то, что назвал зовущий или что записано в конвенциях проекта.
|
||||
|
||||
<!-- /копия: порог-правки -->
|
||||
|
||||
И обратное: язык правится **по ходу той операции, которая записи касается**.
|
||||
Беклог не переписывают ради языка.
|
||||
@@ -0,0 +1,34 @@
|
||||
# Сопровождение и эксплуатация
|
||||
|
||||
**Копия.** Дом — `shared/operations.md` в репозитории плагинов. Словарь общий для
|
||||
роадмапа, архитектуры и темы ревью `operations`, и не принадлежит ни одному из
|
||||
трёх — правится дом, а не этот файл.
|
||||
|
||||
Скиллу задач он нужен для секции `Сопровождение` в `ROADMAP.md`: она отвечает не
|
||||
на «что приложение будет уметь», а на «чем его держат», и путать эти два вопроса
|
||||
нельзя.
|
||||
|
||||
<!-- копия: сопровождение-словарь из shared/operations.md -->
|
||||
|
||||
Одна тема живёт в трёх местах, и путать их слова нельзя.
|
||||
|
||||
**Сопровождение** — всё, чем держат проект: инструмент и сборка, процесс,
|
||||
выкладка, метрики и логи, инфраструктура, дежурство. **Эксплуатация** — его
|
||||
часть: работа системы на проде. Целое и часть, и никогда наоборот.
|
||||
|
||||
| Место | Уровень | Что там |
|
||||
| --- | --- | --- |
|
||||
| `ROADMAP.md`, секция `Сопровождение` | план | **работы**, которые собираемся делать: цели и их задачи |
|
||||
| `architecture.md`, раздел «Эксплуатация» | состояние | **как устроено сейчас**: где работает, что рядом, кто перезапускает |
|
||||
| тема ревью `operations` | оптика | **чем проверяем**: «это упало через неделю на проде» |
|
||||
|
||||
Слово **«поддержка» не употребляется вовсе** — в нём слышится помощь
|
||||
пользователю, а это другая работа.
|
||||
|
||||
**Граница с возможностями проходит по тому, кто наблюдает.** «Приложение
|
||||
сообщает о своём состоянии» — возможность приложения, её место среди прочих
|
||||
целей: наблюдает пользователь сервиса. «Дежурный видит состояние на одном
|
||||
экране» — сопровождение: наблюдаем мы. Одни и те же метрики попадают в разные
|
||||
секции роадмапа, и это верно — секции отвечают на разные вопросы.
|
||||
|
||||
<!-- /копия: сопровождение-словарь -->
|
||||
+11
-8
@@ -57,22 +57,25 @@
|
||||
`REJECTED.md` здесь — не «выкинули», а именно тот след, что переживает запись:
|
||||
через квартал вопрос «куда делась задача X» отвечается строкой со ссылками на
|
||||
наследников, а не археологией git;
|
||||
- родитель осмыслен как **возможность**, а не как шаг → это цель. Тип на месте
|
||||
не меняется (цель живёт в другом индексе): заводится `--type goal` в `ROADMAP.md`,
|
||||
части получают `--goal <новый слаг>`, родитель закрывается с причиной-ссылкой.
|
||||
- родитель осмыслен как **возможность**, а не как шаг → это цель, и **строка
|
||||
переезжает**: `edit <slug> --type goal --section <часть роадмапа>` снимает её с
|
||||
`BACKLOG.md` и вставляет в `ROADMAP.md`. Файл в `items/` при этом не двигается —
|
||||
он и есть запись. Части получают `--goal <слаг родителя>`, а закрывать родителя
|
||||
нечем и незачем: он не выкинут, он стал целью.
|
||||
|
||||
**Промежуточного зонтика между целью и задачей нет.** Тип `epic` упразднён:
|
||||
роль зонтика играет цель, а слишком крупный шаг дробится на шаги помельче под
|
||||
той же целью. Если частям нужен общий заголовок — значит у них общая
|
||||
возможность, и её надо назвать целью, а не заводить временный тип.
|
||||
|
||||
## Когда декомпозиция случается посреди спринта
|
||||
## Когда декомпозиция случается посреди работы
|
||||
|
||||
Задача, которая **оказалась крупнее задачи**, распознаётся до того, как под неё
|
||||
заведено предложение об изменении: иначе его придётся выбрасывать. Она выходит
|
||||
из набора (`sprint drop … --reason "крупнее задачи"`), уходит на декомпозицию, а
|
||||
спринт продолжается остальными. Части заводятся сразу под той же целью, но в
|
||||
текущий набор **не добавляются** — набор заморожен.
|
||||
из работы на декомпозицию, а её строка возвращается в беклог с причиной
|
||||
(`move … --reason "крупнее задачи"`). Части заводятся сразу под той же целью, и
|
||||
**место в очереди им назначает человек**: машина поставит их в конец секции, а
|
||||
крупная задача редко распадается на что-то менее срочное, чем была сама.
|
||||
|
||||
## Мозговой штурм сырья
|
||||
|
||||
@@ -80,7 +83,7 @@
|
||||
тест «готова к взятию», потому что неясно, что именно делаем. Штурм проясняет —
|
||||
и это **generative-операция, а не applicative**.
|
||||
|
||||
Исход штурма и есть заполненный «Вопрос» (тогда разведку можно брать в спринт)
|
||||
Исход штурма и есть заполненный «Вопрос» (тогда разведку можно брать в работу)
|
||||
или набор задач с типами, которые из ответа следуют. Третий законный исход —
|
||||
`close --reason`.
|
||||
|
||||
+6
-6
@@ -15,8 +15,8 @@
|
||||
| Допустимые сверх того | `Рамки`, `Вопросы` |
|
||||
| Поле места | **Категория** — полка домена беклога |
|
||||
| Цель (`goal:<слаг>`) | нет: цель — это возможность, а здесь её не появляется |
|
||||
| Индекс | `BACKLOG.md` → `SPRINT.md` |
|
||||
| Берётся в спринт | да |
|
||||
| Индекс | `BACKLOG.md` |
|
||||
| Берётся в работу | да |
|
||||
|
||||
## Адресат — разработчик, и это законно
|
||||
|
||||
@@ -48,13 +48,13 @@
|
||||
5. **Проверить, что это не «заодно».** Обслуживание любит склеиваться в пачку
|
||||
(«обновить зависимости и переписать сборку и убрать мёртвый код»). Не
|
||||
мерджится порознь — это несколько задач ([split.md](split.md)).
|
||||
6. **Цель не проставлять.** `chore` служит работоспособности, а не направлению,
|
||||
и в набор спринта входит помимо его цели. Работа по сопровождению проекта
|
||||
при этом видна в роадмапе — секцией `Сопровождение`, но целью не становится.
|
||||
6. **Цель не проставлять.** `chore` служит работоспособности, а не направлению.
|
||||
Работа по сопровождению проекта при этом видна в роадмапе — секцией
|
||||
`Сопровождение`, но целью не становится.
|
||||
|
||||
## Что видит машина, а что человек
|
||||
|
||||
`check` и `sprint take` смотрят на **наличие непустого** `Затрагивает` и на
|
||||
`ready` смотрит на **наличие непустого** `Затрагивает` и на
|
||||
**число** критериев — ровно то же, что у `feature`. Разница между типами здесь не
|
||||
в строгости проверки, а в том, **кому адресован ответ** на «что станет
|
||||
наблюдаемо иначе», — и это судит человек.
|
||||
+9
-7
@@ -16,12 +16,12 @@
|
||||
| Допустимые сверх того | `Рамки`, `Вопросы` |
|
||||
| Поле места | **Категория** — полка домена беклога |
|
||||
| Цель (`goal:<слаг>`) | **обязательна** |
|
||||
| Индекс | `BACKLOG.md` → `SPRINT.md` |
|
||||
| Берётся в спринт | да |
|
||||
| Индекс | `BACKLOG.md` |
|
||||
| Берётся в работу | да |
|
||||
|
||||
**Цель обязательна, и это единственный тип, у которого так.** Новая возможность
|
||||
и есть содержание цели: подходящей нет — либо она заводится, либо перед тобой не
|
||||
`feature`. `sprint take` без цели откажет.
|
||||
`feature`. `ready` без цели откажет.
|
||||
|
||||
## Алгоритм
|
||||
|
||||
@@ -42,15 +42,17 @@
|
||||
заходом и не мерджится целиком — это несколько задач под одной целью, дроби
|
||||
сразу ([split.md](split.md)). Промежуточного зонтика между целью и задачей
|
||||
нет.
|
||||
6. **Реализация** — дело пайплайна проекта, не этого скилла. Закрывается
|
||||
6. **Реализация** — дело конвейера проекта, не этого скилла. Закрывается
|
||||
`close <слаг> --implemented`: файл и строка удаляются, суть переезжает в
|
||||
`openspec/specs/` и документацию.
|
||||
|
||||
## Что видит машина, а что человек
|
||||
|
||||
`check` и `sprint take` смотрят на **наличие непустого** раздела `Затрагивает`,
|
||||
на **число** критериев (меньше двух — отказ, больше пяти — замечание) и на цель.
|
||||
Наличие оракула проверяется **эвристикой** — словом «оракул» в пункте.
|
||||
Схему типа судит `ready` на входе в работу: **наличие непустого** раздела
|
||||
`Затрагивает`, **число** критериев (меньше двух — отказ, больше пяти —
|
||||
замечание) и цель. Наличие оракула проверяется **эвристикой** — словом «оракул»
|
||||
в пункте. `check` этого поимённо не говорит, а считает строкой здоровья
|
||||
(`SKILL.md`, «Что механизировано, а что нет»).
|
||||
|
||||
Полнота перечня границ машине не видна: границу, которую забыли назвать, она от
|
||||
отсутствующей не отличает. Настоящий оракул от слова «оракул» тоже не отличает.
|
||||
+6
-7
@@ -17,14 +17,14 @@
|
||||
| Допустимые сверх того | `Рамки`, `Вопросы` |
|
||||
| Поле места | **Категория** — полка домена беклога |
|
||||
| Цель (`goal:<слаг>`) | необязательна |
|
||||
| Индекс | `BACKLOG.md` → `SPRINT.md` |
|
||||
| Берётся в спринт | да |
|
||||
| Индекс | `BACKLOG.md` |
|
||||
| Берётся в работу | да |
|
||||
|
||||
## `Воспроизведение` — раздел, которого нет у других типов
|
||||
|
||||
**Не воспроизводится — это `research`, а не `fix`.** Правило было записано и
|
||||
раньше, но проверять его было нечем, и «починки» без единого шага повторения
|
||||
уходили в спринт наравне с остальными. Раздел делает правило проверяемым: он
|
||||
уходили в работу наравне с остальными. Раздел делает правило проверяемым: он
|
||||
называет, **что сделать, чтобы расхождение проявилось, и что при этом видно
|
||||
вместо ожидаемого**.
|
||||
|
||||
@@ -54,9 +54,8 @@
|
||||
почти всегда есть парный критерий: **прежнее поведение не сломалось**
|
||||
(«ввод `а1` принимается по-прежнему»). Без него починка чинит одно и ломает
|
||||
соседнее.
|
||||
6. **Цель не выдумывать.** `fix` служит работоспособности, а не направлению, и в
|
||||
набор спринта входит помимо его цели. Придуманная цель — то же враньё, от
|
||||
которого спасает тип.
|
||||
6. **Цель не выдумывать.** `fix` служит работоспособности, а не направлению.
|
||||
Придуманная цель — то же враньё, от которого спасает тип.
|
||||
7. **Записать дефект в журнал** `docs/review.md` с пометкой «проскочил / пойман
|
||||
ревью». Проскочившие — проверочный набор для калибровки конвейера; пойманные с
|
||||
оракулом — лучшая опора для прохода ревью: проектные, воспроизводимые,
|
||||
@@ -64,7 +63,7 @@
|
||||
|
||||
## Что видит машина, а что человек
|
||||
|
||||
`check` и `sprint take` смотрят на **наличие непустого** `Воспроизведения` и
|
||||
`ready` смотрит на **наличие непустого** `Воспроизведения` и
|
||||
`Затрагивает` и на **число** критериев. Годность воспроизведения — человеку:
|
||||
шаги, по которым ничего не воспроизводится, машина от годных не отличает, и
|
||||
делать вид, что проверено больше проверенного, хуже, чем не проверять вовсе.
|
||||
+21
-41
@@ -23,9 +23,9 @@
|
||||
# 🐞 Не отбрасывать молча лишние символы в ходе
|
||||
|
||||
- **Тип:** fix
|
||||
- **Категория:** Ядро — вышла из спринта: остаток писал нерешённое в журнал
|
||||
- **Категория:** Ядро — вернулась из работы: остаток писал нерешённое в журнал
|
||||
- **Зачем:** ввод «а1б2» ходит в a1 — игрок не видит, что ошибся, и винит игру
|
||||
- **Теги:** goal:merge-robustness, sprint:2026-08-03
|
||||
- **Теги:** goal:merge-robustness
|
||||
|
||||
Разбор хода читает первые два символа и молча выбрасывает остаток строки.
|
||||
|
||||
@@ -63,11 +63,11 @@
|
||||
здоровье; годность формулировки смотрит агент `task-form`.
|
||||
- **Мета-блок** — список сразу после заголовка, **поле на строку**. Обязательны
|
||||
**тип** и **место**, причина после тире желательна (именно она объясняет,
|
||||
почему задача здесь оказалась — в том числе «вышла из спринта: …»), «зачем» и
|
||||
почему задача здесь оказалась — в том числе «вернулась из работы: …»), «зачем» и
|
||||
теги необязательны. Нераспознанные поля сохраняются: скрипт правит свои и не
|
||||
трогает чужие.
|
||||
- **Тип — первым полем.** Он решает, что у записи вообще может быть: какие
|
||||
разделы обязательны, нужна ли цель, берётся ли она в спринт, — и читается
|
||||
разделы обязательны, нужна ли цель, берётся ли она в работу, — и читается
|
||||
раньше всего остального. Словарь **закрыт**: `goal` | `feature` | `fix` |
|
||||
`chore` | `research`. Не подходит ни один — это сигнал, что в записи их два и
|
||||
её надо разделить.
|
||||
@@ -93,11 +93,11 @@
|
||||
| Тип | Поле | Значения | Что это |
|
||||
| --- | --- | --- | --- |
|
||||
| `goal` | **Секция** | `Запланировано`, `Направления`, `Сопровождение` | часть роадмапа: состояние очереди |
|
||||
| прочие | **Категория** | секции беклога проекта (`Ядро`, `Инфра`, …) | полка домена, в которую задача вернётся из спринта |
|
||||
| прочие | **Категория** | секции беклога проекта (`Ядро`, `Инфра`, …) | полка домена, на которой задача лежит |
|
||||
|
||||
Разные имена потому, что это **разные вещи**. У задачи поле переживает спринт:
|
||||
`sprint drop` возвращает её именно туда. У цели оно называет не полку, а место в
|
||||
очереди работ. Одно имя на два смысла их и смешивало; `check` называет
|
||||
Разные имена потому, что это **разные вещи**. У задачи это полка: куда её
|
||||
положили и куда вернут, если она уйдёт в работу и вернётся. У цели оно называет
|
||||
не полку, а место в очереди работ. Одно имя на два смысла их и смешивало; `check` называет
|
||||
несовпадение дрейфом, `check --fix` переименовывает.
|
||||
|
||||
Имя самого места принадлежит **заголовку индекса** — файл на него лишь
|
||||
@@ -142,7 +142,7 @@
|
||||
имя таблицы стабильно, номер последней миграции протухает молча. Пишется
|
||||
`таблица points и её миграция`, а не `миграция 0042`.
|
||||
|
||||
**Что из этого механизировано.** `check` и `sprint take` смотрят только на
|
||||
**Что из этого механизировано.** `ready` смотрит только на
|
||||
**наличие непустого раздела**. Полнота перечня машине не видна: границу, которую
|
||||
забыли назвать, она от отсутствующей не отличает. Раздела нет — отказ во взятии:
|
||||
оценивать нечем.
|
||||
@@ -154,11 +154,11 @@
|
||||
|
||||
2–5 проверяемых утверждений **списком** `- …`, **у каждого назван оракул**. Не
|
||||
«работает корректно», а «повторный прогон даёт тот же отпечаток — оракул:
|
||||
команда сверки». Это не второе определение готовности, а проектная
|
||||
команда сверки». Это не второе определение сделанного, а проектная
|
||||
конкретизация вопроса «по чему видно, что закончено» из теста готовности ниже:
|
||||
там сказано «признак завершённости», здесь — «признак плюс чем проверяется».
|
||||
|
||||
**Что из этого механизировано.** `check` и `sprint take` считают пункты: меньше
|
||||
**Что из этого механизировано.** `ready` считает пункты: меньше
|
||||
двух — отказ («— работает» одной строкой больше не проходит), больше пяти —
|
||||
замечание, обычно это признак, что задача крупнее задачи. Наличие оракула
|
||||
проверяется **эвристикой** — словом «оракул» в пункте, — и потому даёт только
|
||||
@@ -205,8 +205,8 @@
|
||||
ответом. Затем снимается тег (`edit <slug> --rm-tag question`) и переписывается
|
||||
«зачем»: «Решено: …» на вопрос «зачем нужна эта задача» уже не отвечает.
|
||||
|
||||
**Порядок именно такой, потому что судит раздел, а не тег.** `sprint take`
|
||||
смотрит в непустой раздел и откажет взять задачу даже со снятым тегом, а `check`
|
||||
**Порядок именно такой, потому что судит раздел, а не тег.** `ready`
|
||||
смотрит в непустой раздел и откажет даже при снятом теге, а `check`
|
||||
на снятый тег при непустом разделе посоветует тег вернуть. Снять тег, не
|
||||
опустошив раздел, — значит закольцевать себя между двумя советами.
|
||||
|
||||
@@ -240,7 +240,7 @@
|
||||
`check` напоминает о нём у цели без задач замечанием — неразобранная цель
|
||||
законна и зелёного прогона не ломает; `check --fix` сам ставит его цели, у
|
||||
которой задачи есть, а цель с тегом и без задач — прямое приглашение закрыть.
|
||||
- Цель живёт в `ROADMAP.md` и **никогда** — в `BACKLOG.md` или `SPRINT.md`.
|
||||
- Цель живёт в `ROADMAP.md` и **никогда** — в `BACKLOG.md`.
|
||||
- **Достигнутая цель не исчезает.** `close <слаг> --implemented` удаляет файл и
|
||||
переносит строку в секцию `Готово` с датой:
|
||||
`- 2026-08-04 \`merge-order\` — Исход слияния не зависит от порядка доставки. …`
|
||||
@@ -277,33 +277,23 @@
|
||||
| Файл | Что отвечает | Секции |
|
||||
| --- | --- | --- |
|
||||
| `ROADMAP.md` | что приложение уже умеет и чего ещё не умеет | канонические и в этом порядке: `Запланировано`, `Направления`, `Сопровождение`, `Готово` (англ. `Planned`, `Directions`, `Operations`, `Done`) |
|
||||
| `BACKLOG.md` | что **можно взять** — только задачи | категории проекта (по умолчанию Ядро/Инфра) |
|
||||
| `SPRINT.md` | какая цель (или что её нет) и какой набор заморожен | одна: «Набор» |
|
||||
| `BACKLOG.md` | что **можно взять** — только задачи, **в порядке очереди** | категории проекта (по умолчанию Ядро/Инфра) |
|
||||
| `REJECTED.md` | что ушло без реализации и почему | — |
|
||||
|
||||
Шапку `SPRINT.md` пишет `sprint start` — **тем же мета-блоком, что у задачи**:
|
||||
поле на строку, `- **Цель:** [Заголовок](items/slug.md)`, `- **Начат:**` датой,
|
||||
`- **Спринт:**` слагом, которым метится урожай. У спринта без цели
|
||||
(`sprint start --no-goal`) поле «Цель» остаётся на месте и пишется прозой без
|
||||
ссылки — «не названа»: **«цели нет» и «цель потерялась» обязаны различаться**.
|
||||
Поэтому и признак «спринт идёт» — слаг, а не цель: слаг есть у любого спринта,
|
||||
без него нечем метить урожай. Прежняя форма (три поля одной
|
||||
строкой через `·`) читается по-прежнему и уходит сама: файл переписывается на
|
||||
следующем `sprint start` и очищается на `sprint close`.
|
||||
|
||||
Секции — **единственные заголовки `##` в индексе**: любой другой `##` в
|
||||
преамбуле проверка сочтёт секцией.
|
||||
|
||||
**Порядка «по важности» внутри секции беклога нет** — «что делать дальше»
|
||||
отвечает набор спринта. Единственный порядок, который есть, **производен от типа
|
||||
и заполненности**: **сырьё** (`research` без раздела «Вопрос») стоит в конце
|
||||
**Порядок строк внутри секции беклога значим: это очередь.** Первая строка — то,
|
||||
что делают следующим; назначает порядок человек на груминге, и двигают его
|
||||
`move --after` и `move --first`. Одно место из очереди изъято и **производно от
|
||||
типа и заполненности**: **сырьё** (`research` без раздела «Вопрос») стоит в конце
|
||||
своей секции, потому что его не берут, и между берущимся оно каждый раз требует
|
||||
открыть файл, чтобы это понять. Проверяет `check`, переставляет `check --fix`,
|
||||
и человек этот порядок не назначает — иначе он был бы приоритетом, которого
|
||||
здесь нет.
|
||||
|
||||
**Секции «блокеры» среди них нет.** Блокер — состояние, а не полка: он живёт до
|
||||
ответа человека, а следы остаются вопросами в файлах задач распущенного спринта.
|
||||
ответа человека, а следы остаются вопросами в файлах задач.
|
||||
Постоянно пустая секция со старой семантикой «разбираются пачками» противоречила
|
||||
бы правилу «эскалируем немедленно», поэтому `init` её не заводит, а `check`
|
||||
говорит о ней в чужом беклоге. Переезжаешь с такой секцией — удали её. В секции
|
||||
@@ -333,9 +323,6 @@ SKILL.md. Порядок закреплён потому, что `Готово`
|
||||
Поэтому отказ на второй задаче из пяти не оставляет первую переписанной при
|
||||
нетронутых индексах.
|
||||
|
||||
`SPRINT.md` и есть артефакт заморозки: без него набор существует только в
|
||||
контексте сессии, и нарушение заморозки ненаблюдаемо.
|
||||
|
||||
## `REJECTED.md`
|
||||
|
||||
Туда уходит задача, покинувшая беклог **без реализации**. Строку пишет
|
||||
@@ -361,15 +348,8 @@ SKILL.md. Порядок закреплён потому, что `Готово`
|
||||
|
||||
- `goal:<слаг>` — цель, которой служит задача. Обязателен **у `feature`**:
|
||||
новая возможность и есть содержание цели. У `fix`, `chore` и `research` его
|
||||
может не быть — они служат работоспособности, а не направлению, и в набор
|
||||
спринта входят помимо его цели.
|
||||
может не быть — они служат работоспособности, а не направлению.
|
||||
- `question` — в файле есть неразобранный раздел «Вопросы».
|
||||
- `sprint:<слаг>` — задача заведена в этом спринте; по нему отбирается первая
|
||||
порция разбора («урожай спринта»). **Ставится сам**: слаг спринта заводит
|
||||
`sprint start` (по умолчанию — дата начала, он же пишется в `SPRINT.md`), и
|
||||
`add` при открытом спринте помечает заводимое. Тег, который надо помнить
|
||||
ставить руками, не ставится никогда — а на нём висит правило «первая порция
|
||||
разбора — урожай прошедшего спринта».
|
||||
- `decomposed` — на цели: разложена на задачи (см. «Файл цели»).
|
||||
|
||||
Тега `kind:<род>` больше нет: род работы стал типом. Оставшийся в файле `check`
|
||||
+8
-8
@@ -16,11 +16,11 @@
|
||||
| Допустимые сверх того | — |
|
||||
| Поле места | **Секция** — часть роадмапа |
|
||||
| Цель (`goal:<слаг>`) | запрещена: цель и есть цель |
|
||||
| Индекс | `ROADMAP.md`, и никогда `BACKLOG.md` или `SPRINT.md` |
|
||||
| Берётся в спринт | нет — берутся её задачи |
|
||||
| Индекс | `ROADMAP.md`, и никогда `BACKLOG.md` |
|
||||
| Берётся в работу | нет — берутся её задачи |
|
||||
|
||||
Поле места у цели называется **«Секция»**, а не «Категория», и это не разнобой:
|
||||
у задачи оно называет полку домена, в которую она вернётся из спринта, а у цели
|
||||
у задачи оно называет полку домена, на которой она лежит, а у цели
|
||||
— часть роадмапа, то есть состояние очереди. Одно имя на два смысла их и
|
||||
смешивало.
|
||||
|
||||
@@ -38,8 +38,8 @@
|
||||
|
||||
1. **Проверить, что это возможность, а не работа.** Работа, которой держат
|
||||
проект, на вопрос «что приложение будет уметь» не отвечает; состав перечислен
|
||||
[в каноне](../../canon/references/canon.md), раздел «Сопровождение и
|
||||
эксплуатация». Ей отведена секция `Сопровождение` — там она видна в том же
|
||||
[в словаре сопровождения](operations.md). Ей отведена секция
|
||||
`Сопровождение` — там она видна в том же
|
||||
экране и не читается как обещание продукта. Граница проходит по тому,
|
||||
**кто наблюдает**:
|
||||
«приложение сообщает о своём состоянии» — возможность, «дежурный видит
|
||||
@@ -75,9 +75,9 @@
|
||||
не попадает: `Готово` отвечает «что приложение умеет», а отменённая цель не
|
||||
умеет ничего.
|
||||
|
||||
**Место этому — переоценка на сессии, а не отдельный заход.** Отмена цели значит
|
||||
разбор всех её задач, а разбор задач и есть шаг 3 сессии
|
||||
([cadence.md](../../session/references/cadence.md), пункт 7). Отменять на ходу,
|
||||
**Место этому — груминг, а не отдельный заход.** Отмена цели значит
|
||||
разбор всех её задач, а разбор задач и есть шаг 3 груминга
|
||||
(скилл `groom`, «что перестало быть важным»). Отменять на ходу,
|
||||
между делом, — верный способ закрыть скопом то, что стоило перевесить.
|
||||
|
||||
## Что видит машина, а что человек
|
||||
+11
-10
@@ -15,8 +15,8 @@
|
||||
| Допустимые сверх того | `Рамки`, `Вопросы` |
|
||||
| Поле места | **Категория** — полка домена беклога |
|
||||
| Цель (`goal:<слаг>`) | нет |
|
||||
| Индекс | `BACKLOG.md` → `SPRINT.md` |
|
||||
| Берётся в спринт | да — **но только с заполненным «Вопросом»** |
|
||||
| Индекс | `BACKLOG.md` |
|
||||
| Берётся в работу | да — **но только с заполненным «Вопросом»** |
|
||||
|
||||
**Критериев приёмки у `research` нет, и это не поблажка.** Критерии в форме
|
||||
«оракул: тест» разведке натянуты: проверять нечего, пока ответа нет. Её приёмка
|
||||
@@ -39,13 +39,14 @@
|
||||
| | сырьё | разведка |
|
||||
| --- | --- | --- |
|
||||
| Раздел `Вопрос` | пуст или отсутствует | заполнен |
|
||||
| `sprint take` | отказ | берёт |
|
||||
| `ready` | отказ | берёт |
|
||||
| Место в секции беклога | **конец**, `check --fix` сносит туда сам | среди прочих |
|
||||
| `tasks.py list --raw` | показывает | нет |
|
||||
|
||||
Порядка «по важности» в беклоге по-прежнему нет. Этот порядок **производен от
|
||||
типа и заполненности**, а не назначен человеком, — потому его и проверяет машина,
|
||||
и потому он не противоречит правилу «порядка нет, есть цель».
|
||||
Порядок строк в беклоге назначает человек — это приоритет (правило 4 скилла).
|
||||
Место сырья **из него изъято**: оно производно от типа и заполненности, а не от
|
||||
чьего-то решения, и потому его проверяет и чинит машина. Приоритетом оно не
|
||||
становится: сырьё не берут вовсе, и место в конце говорит именно это.
|
||||
|
||||
Сырьём заводится и **сырая функция**: «Подсказка следующего хода» — ещё не
|
||||
`feature`, потому что неизвестно, что именно делать. Работа над ней — думание, и
|
||||
@@ -67,16 +68,16 @@
|
||||
проход ревью обязан читать как условие, а не как замер.
|
||||
5. **Разложить исход на задачи** — если он их родил. Разведка кончается одним из
|
||||
трёх: заведены задачи, записано знание, отказ. **Отказ — полноправный исход**:
|
||||
«проверили, не проблема» экономит спринт.
|
||||
«проверили, не проблема» экономит работу.
|
||||
6. **Закрыть** — `close <слаг> --implemented`, когда ответ записан. Файл
|
||||
удаляется: запись ответа и есть след, второго не нужно. Ушла без ответа —
|
||||
`close --reason`, и строка уезжает в `REJECTED.md`.
|
||||
|
||||
## Что видит машина, а что человек
|
||||
|
||||
`check` и `sprint take` смотрят на **наличие непустых** разделов `Вопрос` и
|
||||
`Куда ляжет ответ`, считают сырьё отдельной строкой здоровья и держат его в конце
|
||||
секции. Годность вопроса — человеку: «вопрос это или тема» машина не различает,
|
||||
`ready` смотрит на **наличие непустых** разделов `Вопрос` и
|
||||
`Куда ляжет ответ`; `check` считает сырьё отдельной строкой здоровья и держит
|
||||
его в конце секции. Годность вопроса — человеку: «вопрос это или тема» машина не различает,
|
||||
и `check` о годности молчит намеренно.
|
||||
|
||||
Штурм сырья, дробление исхода на задачи и тест «части мерджатся порознь» —
|
||||
+279
-520
File diff suppressed because it is too large
Load Diff
+15
-3
@@ -9,7 +9,7 @@
|
||||
#
|
||||
# **Что судится — staged-файлы, а не рабочее дерево**, всюду, где проверка
|
||||
# умеет смотреть поимённо: гейт обязан судить то, что уедет в историю, а не то,
|
||||
# что случайно лежит на диске рядом. Два исключения названы у своих задач, и оба
|
||||
# что случайно лежит на диске рядом. Три исключения названы у своих задач, и все
|
||||
# — про то, что проверке нужен весь репозиторий по существу, а не для удобства.
|
||||
#
|
||||
# Ставится `lefthook install` (см. README, «Гейт коммита»). Обойти разово —
|
||||
@@ -23,15 +23,27 @@ pre-commit:
|
||||
# copies.py сверяет копию с домом, а дом лежит в другом файле, которого в
|
||||
# индексе может не быть: список staged дал бы «копии дословны» там, где
|
||||
# правка дома их и разошлась. frontmatter.py смотрел бы поимённо, но весь
|
||||
# обход стоит сотые доли секунды — платить за него нечем.
|
||||
# обход стоит сотые доли секунды — платить за него нечем. Glob у него шире
|
||||
# на `*.json`: тем же проходом сверяется `description` плагина в
|
||||
# `plugin.json` с записью того же плагина в `marketplace.json`, а коммит,
|
||||
# правящий только манифест, по глобу `*.md` проверку бы не разбудил.
|
||||
- name: фронтматтеры
|
||||
glob: "*.md"
|
||||
glob: "*.{md,json}"
|
||||
run: python3 scripts/frontmatter.py
|
||||
|
||||
- name: копии правил
|
||||
glob: "*.md"
|
||||
run: python3 scripts/copies.py
|
||||
|
||||
# Без glob намеренно, и это третье исключение из правила «судим staged».
|
||||
# Проверка сводит две стороны: перечень адресов лежит в константе скрипта
|
||||
# владельца (`.py`), упоминания — в прозе плагинов (`.md`). Коммит, где
|
||||
# переименован документ канона, трогает только первую сторону: по глобу
|
||||
# `*.md` он бы проверку не разбудил, а расходится в нём именно вторая.
|
||||
# Весь обход — семь сотых секунды.
|
||||
- name: адреса документов
|
||||
run: python3 scripts/addresses.py
|
||||
|
||||
# Самая дорогая проверка: каждый блок — свой запуск mermaid-cli со своим
|
||||
# chromium. Отсюда и staged-файлы вместо обхода, и параллель внутри самого
|
||||
# скрипта: репозиторий целиком — 3 секунды, один файл — одна.
|
||||
|
||||
+9
-2
@@ -56,10 +56,17 @@ quote-style = "double"
|
||||
|
||||
[tool.pyrefly]
|
||||
project-includes = [
|
||||
"av-dev-pm/skills/tasks/scripts/tasks.py",
|
||||
"av-dev-pm/skills/canon/scripts/docs.py",
|
||||
"av-dev-tasks/skills/tasks/scripts/tasks.py",
|
||||
"av-dev-docs/skills/canon/scripts/docs.py",
|
||||
"av-dev-code/skills/openspec/scripts/openspec.py",
|
||||
"scripts/addresses.py",
|
||||
"scripts/copies.py",
|
||||
"scripts/diagrams.py",
|
||||
"scripts/frontmatter.py",
|
||||
"scripts/resync.py",
|
||||
]
|
||||
python-version = "3.12"
|
||||
# resync.py импортирует разборщик разметки из copies.py: второй разборщик той же
|
||||
# разметки разошёлся бы с первым молча. Оба лежат в scripts/ и в чужой проект не
|
||||
# уезжают, поэтому импорт между ними законен — но искать его надо здесь.
|
||||
search-path = ["scripts"]
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Сверка чужих адресов в прозе плагинов с перечнем их владельца.
|
||||
|
||||
Судится **упразднённое, а не незнакомое**, и это следует из канона, а не из
|
||||
осторожности: список тем открытый — всё, что проект кладёт в `docs/` сверх
|
||||
закрытых категорий, законная тема. Значит незнакомое имя опровергнуть нечем, а
|
||||
переименование и упразднение ловятся точно: канон, убирая слот, кладёт его в
|
||||
карту переездов, и именно она здесь и есть перечень запрещённого. Рядом
|
||||
единственная догадка — имя, **почти** совпавшее с каноническим: это опечатка с
|
||||
куда большей вероятностью, чем новая тема.
|
||||
|
||||
Адрес документа принадлежит одному плагину, а называют его все: `docs/*` стоит
|
||||
примерно в сорока местах `av-dev-code`, `tasks/ROADMAP.md` — в четырёх местах
|
||||
`av-dev-docs`. Переименование в каноне до этих мест не доходит.
|
||||
|
||||
**Почему тут нужна машина, а не аккуратность.** Прогон ревью умеет честно
|
||||
деградировать: дома темы нет — в границах покрытия появляется строка «документа в
|
||||
проекте нет» с названной ценой. Протухший адрес попадает ровно в эту машинерию —
|
||||
файл не открылся, строка напечаталась, и отчёт выглядит добросовестным. То есть
|
||||
единственный признак ошибки, на который можно было бы рассчитывать — громкая
|
||||
поломка, — деградацией и убран. Здесь он возвращается гейтом.
|
||||
|
||||
Перечень адресов берётся из **константы владельца** — той самой, по которой он и
|
||||
так проверяет раскладку. Второй перечень прозой был бы вторым домом ровно того
|
||||
сорта, против которого всё это написано.
|
||||
|
||||
addresses.py [корень]
|
||||
|
||||
Коды выхода — общий словарь скриптов av-dev:
|
||||
0 сошлось
|
||||
1 дрейф: неизвестный или упразднённый адрес
|
||||
2 ошибка употребления
|
||||
3 окружение: не тот каталог, перечень владельца недоступен
|
||||
4 внутренний сбой
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import difflib
|
||||
import importlib.util
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from types import ModuleType
|
||||
|
||||
OK, DRIFT, USAGE, ENV, INTERNAL = 0, 1, 2, 3, 4
|
||||
|
||||
SKIP_DIRS = {".git", ".venv", "node_modules", "__pycache__", "tmp"}
|
||||
|
||||
# Владельцы: префикс адреса → скрипт, который этим каталогом и владеет.
|
||||
OWNERS = {
|
||||
"docs": "av-dev-docs/skills/canon/scripts/docs.py",
|
||||
"tasks": "av-dev-tasks/skills/tasks/scripts/tasks.py",
|
||||
}
|
||||
|
||||
# Журналы: описывают прошлые состояния и задним числом не переписываются.
|
||||
# Адрес, верный на момент записи, здесь останется навсегда, и это не дрейф.
|
||||
JOURNALS = {
|
||||
"av-dev-docs/skills/canon/references/changelog.md": "журнал версий канона",
|
||||
"DECISIONS.md": "журнал решений",
|
||||
"HISTORY.md": "журнал работ",
|
||||
"NOTES.md": "рабочие заметки",
|
||||
}
|
||||
|
||||
# Файлы, где упразднённый адрес назван по делу: карта переездов и сценарии
|
||||
# перевода чужой раскладки. Неизвестные адреса в них проверяются как везде.
|
||||
RETIRED_OK = {
|
||||
"av-dev-docs/skills/canon/references/canon.md": "карта упразднённых слотов",
|
||||
"av-dev-docs/skills/canon/SKILL.md": "adopt: что где искать в чужой раскладке",
|
||||
"av-dev-tasks/skills/tasks/references/adopt.md": "перевод чужого каталога задач",
|
||||
}
|
||||
|
||||
# Адрес в прозе: начало токена, префикс владельца, остаток пути. Отрицательный
|
||||
# просмотр назад отсекает хвосты чужих путей — `openspec/changes/…/tasks.md`
|
||||
# адресом каталога задач не является.
|
||||
ADDRESS = re.compile(r"(?<![\w/.-])(docs|tasks)/([\w./*-]*)")
|
||||
|
||||
# Порог близости к каноническому имени, за которым имя читается как опечатка, а
|
||||
# не как своя тема проекта. Замер по именам, встреченным в репозитории: самое
|
||||
# близкое законное — `recognition` против `conventions`, 0.64; опечатки
|
||||
# (`architeture`, `securty`, `revew`, `datbase`) дают 0.91–0.96. Порог стоит в
|
||||
# пустоте между ними, и запас с обеих сторон больше 0.15.
|
||||
NEAR = 0.8
|
||||
|
||||
|
||||
def load(root: Path, rel: str) -> ModuleType:
|
||||
"""Скрипт владельца как модуль: константы берутся у него, а не рядом."""
|
||||
path = root / rel
|
||||
name = f"владелец_{path.stem}"
|
||||
spec = importlib.util.spec_from_file_location(name, path)
|
||||
if spec is None or spec.loader is None:
|
||||
raise OSError(f"не читается {rel}")
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
# Модуль обязан лежать в sys.modules **до** исполнения: `@dataclass` внутри
|
||||
# ищет там своё пространство имён и без этого падает.
|
||||
sys.modules[name] = mod
|
||||
spec.loader.exec_module(mod)
|
||||
return mod
|
||||
|
||||
|
||||
def stem(name: str) -> str:
|
||||
"""Имя документа без формы: файл, каталог и `.*` — один и тот же адрес.
|
||||
|
||||
Форму дома канон оставляет проекту: `docs/security.md` и `docs/security/`
|
||||
называют одно. Скрытые имена (`.pm.json`) остаются как есть — точка в них
|
||||
не расширение.
|
||||
"""
|
||||
name = name.rstrip(".")
|
||||
if name.startswith("."):
|
||||
return name.lower()
|
||||
return name.split(".", 1)[0].lower()
|
||||
|
||||
|
||||
def vocabularies(root: Path) -> tuple[dict[str, set[str]], dict[str, str]]:
|
||||
"""Что владельцы считают своим: префикс → имена, плюс карта упразднённого."""
|
||||
docs = load(root, OWNERS["docs"])
|
||||
tasks = load(root, OWNERS["tasks"])
|
||||
|
||||
docs_names = {stem(n) for n in docs.DOCS}
|
||||
docs_names |= {stem(n) for n in docs.CONDITIONAL_DOCS}
|
||||
docs_names |= {stem(n) for n in docs.NOT_DOCS}
|
||||
# `docs/.pm.json` объявлен обязательным файлом вне раскладки.
|
||||
docs_names |= {stem(Path(p).name) for p in docs.REQUIRED if p.startswith("docs/")}
|
||||
|
||||
tasks_names = {stem(tasks.DEFAULTS[k]) for k in tasks.PATH_KEYS}
|
||||
tasks_names |= {stem(tasks.CONFIG_NAME)}
|
||||
|
||||
retired = {stem(n): why for n, why in docs.RETIRED.items()}
|
||||
return {"docs": docs_names, "tasks": tasks_names}, retired
|
||||
|
||||
|
||||
def walk(root: Path) -> list[Path]:
|
||||
out = []
|
||||
for p in sorted(root.rglob("*.md")):
|
||||
rel = p.relative_to(root)
|
||||
if SKIP_DIRS & set(rel.parts):
|
||||
continue
|
||||
if rel.as_posix() in JOURNALS:
|
||||
continue
|
||||
out.append(p)
|
||||
return out
|
||||
|
||||
|
||||
def main() -> int:
|
||||
root = Path(sys.argv[1] if len(sys.argv) > 1 else ".").resolve()
|
||||
if not (root / ".claude-plugin").is_dir():
|
||||
print(f"ОТКАЗ: {root} не похож на корень маркетплейса: нет .claude-plugin/",
|
||||
file=sys.stderr)
|
||||
return ENV
|
||||
|
||||
try:
|
||||
known, retired = vocabularies(root)
|
||||
except Exception as e: # noqa: BLE001 — перечень владельца обязан быть доступен
|
||||
print(f"ОТКАЗ: перечень адресов не взять у владельца: {e}", file=sys.stderr)
|
||||
return ENV
|
||||
|
||||
findings: list[str] = []
|
||||
files = walk(root)
|
||||
seen = 0
|
||||
own_themes: set[str] = set()
|
||||
for path in files:
|
||||
rel = path.relative_to(root).as_posix()
|
||||
retired_ok = rel in RETIRED_OK
|
||||
for num, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1):
|
||||
for m in ADDRESS.finditer(line):
|
||||
owner, rest = m.group(1), m.group(2)
|
||||
first = rest.split("/", 1)[0]
|
||||
if not first or first.startswith("*"):
|
||||
continue # сам каталог или шаблон по всем документам
|
||||
seen += 1
|
||||
name = stem(first)
|
||||
if name in known[owner]:
|
||||
continue
|
||||
if name in retired:
|
||||
if not retired_ok:
|
||||
findings.append(
|
||||
f"{rel}:{num}: `{m.group(0)}` — слот упразднён,"
|
||||
f" содержимое {retired[name]}")
|
||||
continue
|
||||
near = difflib.get_close_matches(name, sorted(known[owner]),
|
||||
n=1, cutoff=NEAR)
|
||||
if near:
|
||||
findings.append(
|
||||
f"{rel}:{num}: `{m.group(0)}` — у владельца ({owner})"
|
||||
f" такого адреса нет, а «{near[0]}» есть: похоже на опечатку")
|
||||
continue
|
||||
own_themes.add(f"{owner}/{name}")
|
||||
|
||||
print(f"адресов встречено {seen} в {len(files)} файлах;"
|
||||
f" перечни взяты из {', '.join(sorted(OWNERS.values()))}")
|
||||
if findings:
|
||||
print()
|
||||
for f in findings:
|
||||
print(f"РАСХОЖДЕНИЕ {f}")
|
||||
print(f"\nИтог: расхождений {len(findings)}. Правится **упоминание**,"
|
||||
f" а не перечень: перечень — то, по чему владелец проверяет"
|
||||
f" раскладку проекта.")
|
||||
return DRIFT
|
||||
|
||||
print("упразднённых адресов нет")
|
||||
if own_themes:
|
||||
print(f"Имён вне перечня {len(own_themes)}, и они **не судятся** —"
|
||||
f" список тем открытый: {', '.join(sorted(own_themes))}.")
|
||||
print(f"Не проверялось: журналы ({len(JOURNALS)} файла — они описывают"
|
||||
f" прошлые состояния), адреса `openspec/*` (раскладка чужого"
|
||||
f" инструмента, у нас владельца нет), упоминания в комментариях"
|
||||
f" скриптов — сверяется только markdown.")
|
||||
return OK
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
sys.exit(main())
|
||||
except KeyboardInterrupt:
|
||||
sys.exit(INTERNAL)
|
||||
except Exception as e: # noqa: BLE001 — последний рубеж, код 4 по словарю
|
||||
print(f"внутренний сбой ({type(e).__name__}): {e}", file=sys.stderr)
|
||||
sys.exit(INTERNAL)
|
||||
+56
-7
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Проверка фронтматтеров скиллов и charter'ов этого репозитория.
|
||||
"""Проверка фронтматтеров скиллов и charter'ов этого репозитория и описаний плагинов.
|
||||
|
||||
Фронтматтер — единственная часть скилла, которую читает не человек, а загрузчик:
|
||||
по `name` он разрешает вызов, по `description` решает, звать ли скилл вообще.
|
||||
@@ -7,7 +7,7 @@
|
||||
разумную строку, а скилл либо не находится по имени, либо загружается с
|
||||
обрезанным описанием и потому не срабатывает на своих же триггерах.
|
||||
|
||||
Ловится три класса.
|
||||
Ловится четыре класса.
|
||||
|
||||
**Двоеточие с пробелом в описании без кавычек.** В YAML `: ` внутри простого
|
||||
скаляра начинает вложенное отображение — строка «конвейер ревью: гейт, сверка…»
|
||||
@@ -21,13 +21,19 @@
|
||||
|
||||
**Цвет, не отвечающий модели.** Цвет charter'а кодирует **модель**, на которой
|
||||
идёт проход, а не его роль: раскладка — в
|
||||
`av-dev-pipeline/skills/review-pipeline/SKILL.md`, раздел «Модель по проходу».
|
||||
`av-dev-code/skills/review/SKILL.md`, раздел «Модель по проходу».
|
||||
Правило существует ровно затем, чтобы стоимость прогона читалась взглядом по
|
||||
списку агентов, и держаться вниманием оно не может: цвет ставится один раз при
|
||||
заведении charter'а, а модель потом меняется калибровкой.
|
||||
|
||||
**Описание плагина, разошедшееся между манифестами.** У описания два дома:
|
||||
`<плагин>/.claude-plugin/plugin.json` его показывает установленному плагину,
|
||||
корневой `.claude-plugin/marketplace.json` — тому, кто выбирает, ставить ли.
|
||||
Правят обычно один, и разойтись они успели уже трижды из четырёх. `copies.py`
|
||||
этот класс не берёт: он смотрит markdown, а манифест — json.
|
||||
|
||||
Коды выхода — тот же словарь, что у tasks.py, docs.py, copies.py и diagrams.py:
|
||||
0 все фронтматтеры в порядке
|
||||
0 все фронтматтеры и описания в порядке
|
||||
1 расхождение
|
||||
2 ошибка употребления: аргументы
|
||||
3 окружение: не тот каталог
|
||||
@@ -37,12 +43,13 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
OK, DRIFT, USAGE, ENV, INTERNAL = 0, 1, 2, 3, 4
|
||||
|
||||
# Дом раскладки — «Модель по проходу» в review-pipeline/SKILL.md; здесь её
|
||||
# Дом раскладки — «Модель по проходу» в av-dev-code/skills/review/SKILL.md; здесь её
|
||||
# механизация. Порядок цветов — порядок стоимости прогона.
|
||||
PALETTE = {"sonnet": "green", "opus": "yellow"}
|
||||
|
||||
@@ -129,6 +136,40 @@ def collect(root: Path) -> list[tuple[Sheet, str, set[str]]]:
|
||||
return found
|
||||
|
||||
|
||||
def manifests(root: Path) -> list[tuple[str, list[str]]]:
|
||||
"""Описание каждого плагина: `plugin.json` против `marketplace.json`."""
|
||||
market = root / ".claude-plugin" / "marketplace.json"
|
||||
where = market.relative_to(root).as_posix()
|
||||
try:
|
||||
listed = {
|
||||
str(entry.get("name", "")): str(entry.get("description", ""))
|
||||
for entry in json.loads(market.read_text(encoding="utf-8"))["plugins"]
|
||||
}
|
||||
except (OSError, ValueError, KeyError, TypeError) as e:
|
||||
return [(where, [f"манифест маркетплейса не разбирается: {e}"])]
|
||||
|
||||
found: list[tuple[str, list[str]]] = []
|
||||
for plugin in sorted(root.glob("av-*/")):
|
||||
card = plugin / ".claude-plugin" / "plugin.json"
|
||||
rel = card.relative_to(root).as_posix()
|
||||
try:
|
||||
own = json.loads(card.read_text(encoding="utf-8"))
|
||||
except (OSError, ValueError) as e:
|
||||
found.append((rel, [f"манифест плагина не разбирается: {e}"]))
|
||||
continue
|
||||
name = str(own.get("name", plugin.name))
|
||||
if name not in listed:
|
||||
found.append((rel, [
|
||||
f"плагина `{name}` нет в {where} — маркетплейс его не отдаёт"
|
||||
]))
|
||||
elif str(own.get("description", "")) != listed[name]:
|
||||
found.append((rel, [
|
||||
f"`description` разошлось с записью `{name}` в {where}:"
|
||||
f" у описания один текст на два манифеста, и правят обычно один"
|
||||
]))
|
||||
return found
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser(description="Проверка фронтматтеров.")
|
||||
ap.add_argument("--dir", default=".", help="корень репозитория")
|
||||
@@ -150,17 +191,25 @@ def main() -> int:
|
||||
if sheet.parsed:
|
||||
sheet.check(expected, required)
|
||||
|
||||
cards = manifests(root)
|
||||
plugins = len(list(root.glob("av-*/")))
|
||||
|
||||
skills = sum(1 for _, _, required in sheets if required is SKILL_KEYS)
|
||||
print(f"фронтматтеров {len(sheets)}: скиллов {skills},"
|
||||
f" charter'ов {len(sheets) - skills}")
|
||||
print(f"манифестов плагинов {plugins}: описание сверено с marketplace.json")
|
||||
|
||||
broken = [sheet for sheet, _, _ in sheets if sheet.problems]
|
||||
if broken:
|
||||
if broken or cards:
|
||||
print()
|
||||
for sheet in broken:
|
||||
for problem in sheet.problems:
|
||||
print(f"ОШИБКА {sheet.where}\n {problem}")
|
||||
print(f"\nИтог: с ошибками {len(broken)} из {len(sheets)}.")
|
||||
for rel, problems in cards:
|
||||
for problem in problems:
|
||||
print(f"ОШИБКА {rel}\n {problem}")
|
||||
print(f"\nИтог: с ошибками {len(broken) + len(cards)}"
|
||||
f" из {len(sheets) + plugins}.")
|
||||
return DRIFT
|
||||
|
||||
print("все в порядке")
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Пересборка помеченных копий из их домов.
|
||||
|
||||
`copies.py` ловит расхождение, но чинить его руками — та же работа, из-за
|
||||
которой копии и расходятся: правка дома касается семи файлов, и седьмой
|
||||
забывают. Здесь она делается машиной и потому дословна по построению.
|
||||
|
||||
**В гейт коммита этот скрипт не ставится, и это решение, а не недосмотр.**
|
||||
Автоматическая пересборка на коммите протащила бы правку дома в семь файлов
|
||||
мимо глаз автора — а правка дома, чья копия уезжает в репозиторий проекта,
|
||||
обязана ещё и попасть в журнал версий канона. Гейт поэтому только **называет**
|
||||
расхождение, а согласие с ним остаётся действием человека.
|
||||
|
||||
Разметку разбирает `copies.py` — он импортируется целиком. Второй разборщик той
|
||||
же разметки разошёлся бы с первым молча, и это ровно тот класс дефекта, против
|
||||
которого вся механика копий и заведена.
|
||||
|
||||
**Ограда блока кода принадлежит месту, а не дому.** Тело `журнал-дефектов-форма`
|
||||
живёт в доме внутри ```, а в скелете канона лежит внутри чужой, объемлющей
|
||||
ограды — и своей там иметь не должно. Поэтому при пересборке берётся тело дома
|
||||
без крайних оград, а обратно надевается **та ограда, что была у копии**.
|
||||
|
||||
resync.py [корень]
|
||||
|
||||
Коды выхода — общий словарь скриптов av-dev:
|
||||
0 готово (в том числе «нечего пересобирать»)
|
||||
2 ошибка употребления: незакрытый маркер, дубль id, копия без дома
|
||||
3 окружение: не тот каталог
|
||||
4 внутренний сбой
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import copies
|
||||
from copies import ENV, INTERNAL, OK, USAGE, Region
|
||||
|
||||
|
||||
def edges(body: list[str]) -> tuple[list[str], list[str]]:
|
||||
"""Ограда копии, если она есть: её надевают обратно на тело дома."""
|
||||
trimmed = [ln.rstrip() for ln in body]
|
||||
while trimmed and not trimmed[0]:
|
||||
trimmed.pop(0)
|
||||
while trimmed and not trimmed[-1]:
|
||||
trimmed.pop()
|
||||
if len(trimmed) >= 2 and trimmed[0].startswith("```") and trimmed[-1].startswith("```"):
|
||||
return [trimmed[0]], [trimmed[-1]]
|
||||
return [], []
|
||||
|
||||
|
||||
def rebuild(path: Path, targets: list[tuple[Region, Region, str]]) -> None:
|
||||
"""Переписать тела копий в одном файле, идя снизу вверх по номерам строк."""
|
||||
lines = path.read_text(encoding="utf-8").splitlines()
|
||||
for copy, home, home_rel in sorted(targets, key=lambda t: -t[0].line):
|
||||
head, tail = edges(copy.body)
|
||||
# Пустые строки по краям сверке не подлежат и потому принадлежат месту:
|
||||
# у копии внутри объемлющей ограды их нет, у копии в прозе — есть.
|
||||
# Ставим их так, как стояло, иначе пересборка правит и то, чего не
|
||||
# чинила, и дифф перестаёт читаться.
|
||||
lead = [""] if copy.body and not copy.body[0].strip() else []
|
||||
trail = [""] if copy.body and not copy.body[-1].strip() else []
|
||||
body = [*head, *home.normalized(), *tail]
|
||||
start = copy.line - 1 # строка открывающего маркера
|
||||
end = start + len(copy.body) + 1 # строка закрывающего
|
||||
lines[start:end + 1] = [
|
||||
f"<!-- копия: {copy.ident} из {home_rel} -->",
|
||||
*lead, *body, *trail,
|
||||
f"<!-- /копия: {copy.ident} -->",
|
||||
]
|
||||
path.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
root = Path(sys.argv[1] if len(sys.argv) > 1 else ".").resolve()
|
||||
if not (root / ".claude-plugin").is_dir():
|
||||
print(f"ОТКАЗ: {root} не похож на корень маркетплейса: нет .claude-plugin/",
|
||||
file=sys.stderr)
|
||||
return ENV
|
||||
|
||||
errors: list[str] = []
|
||||
homes: dict[str, Region] = {}
|
||||
by_file: dict[Path, list[Region]] = {}
|
||||
for path in copies.walk(root):
|
||||
file_homes, file_copies = copies.scan(path, errors)
|
||||
for h in file_homes:
|
||||
if h.ident in homes:
|
||||
errors.append(f"{h.where}: дом «{h.ident}» уже объявлен"
|
||||
f" в {homes[h.ident].where} — id обязан быть один")
|
||||
continue
|
||||
homes[h.ident] = h
|
||||
if file_copies:
|
||||
by_file[path] = file_copies
|
||||
|
||||
for file_copies in by_file.values():
|
||||
for c in file_copies:
|
||||
if c.ident not in homes:
|
||||
errors.append(f"{c.where}: копия «{c.ident}» без дома —"
|
||||
f" дом либо не помечен, либо переименован")
|
||||
|
||||
if errors:
|
||||
for e in errors:
|
||||
print(f"УПОТРЕБЛЕНИЕ {e}", file=sys.stderr)
|
||||
print("\nПересборка не начата: разметка сломана, и починка её не лечит.",
|
||||
file=sys.stderr)
|
||||
return USAGE
|
||||
|
||||
total, touched = 0, 0
|
||||
for path, file_copies in sorted(by_file.items()):
|
||||
targets = []
|
||||
for c in file_copies:
|
||||
home = homes[c.ident]
|
||||
home_rel = home.path.relative_to(root).as_posix()
|
||||
total += 1
|
||||
same_text = c.normalized() == home.normalized()
|
||||
same_addr = bool(c.declared_home) and home_rel.endswith(
|
||||
c.declared_home.lstrip("./"))
|
||||
if same_text and same_addr:
|
||||
continue
|
||||
targets.append((c, home, home_rel))
|
||||
if not targets:
|
||||
continue
|
||||
rebuild(path, targets)
|
||||
touched += len(targets)
|
||||
rel = path.relative_to(root).as_posix()
|
||||
print(f"{rel}: пересобрано {len(targets)} —"
|
||||
f" {', '.join(t[0].ident for t in targets)}")
|
||||
|
||||
print(f"копий {total}, домов {len(homes)}; пересобрано {touched}")
|
||||
if touched:
|
||||
print("\nПрочитай дифф перед коммитом. Копия, уезжающая в репозиторий "
|
||||
"проекта, тянет ещё и запись в журнал версий канона — её машина "
|
||||
"не напишет.")
|
||||
return OK
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
sys.exit(main())
|
||||
except KeyboardInterrupt:
|
||||
sys.exit(INTERNAL)
|
||||
except Exception as e: # noqa: BLE001 — последний рубеж, код 4 по словарю
|
||||
print(f"внутренний сбой ({type(e).__name__}): {e}", file=sys.stderr)
|
||||
sys.exit(INTERNAL)
|
||||
@@ -0,0 +1,258 @@
|
||||
# Язык проектных текстов
|
||||
|
||||
**Это дом.** Файл не входит ни в один плагин: язык общий для документов канона и
|
||||
для задач, и хранить его внутри одного из них значило бы отдать общее правило во
|
||||
владение половине. Плагины везут **копии**, помеченные разметкой `copies.py`, и
|
||||
расхождение ловит гейт коммита, а не внимание.
|
||||
|
||||
Правится **здесь**. Копия, поправленная у себя, — расхождение, а не правка.
|
||||
|
||||
Три блока, и делятся они по потребителю, а не по теме:
|
||||
|
||||
| Блок | Что в нём | Кто копирует |
|
||||
| --- | --- | --- |
|
||||
| `язык-доктрина` | зачем стиль нужен, что взято сверх устава, что отброшено | справочник языка в плагине |
|
||||
| `язык-правила` | девять правил, по которым судят текст | справочник и уставы вычитки |
|
||||
| `порог-правки` | когда находка не заводится | справочник, уставы вычитки, `task-form` |
|
||||
|
||||
`порог-правки` вынесен из правил намеренно: он нужен и тому, кто правил языка не
|
||||
проверяет вовсе, — а вложенных блоков разметка не знает, и внутри `язык-правила`
|
||||
его было бы не забрать отдельно.
|
||||
|
||||
<!-- дом: язык-доктрина -->
|
||||
|
||||
Правила — для всего, что пишется словами: задачи и цели, документы канона,
|
||||
решения ADR, записки разведки, сообщения коммитов. Не для кода и не для
|
||||
сообщений программы пользователю — там свои конвенции проекта.
|
||||
|
||||
Основа — **информационный стиль** Максима Ильяхова ([учебник
|
||||
бюро](https://bureau.ru/projects/book-text/), книга «Пиши, сокращай»). Он
|
||||
написан для рекламы, статей и писем, поэтому взят не целиком.
|
||||
|
||||
## Зачем он здесь
|
||||
|
||||
Проектный текст читают в двух положениях, и оба неудобные: **выбирают, брать ли
|
||||
задачу**, глядя в строку индекса и один экран тела; и **возвращаются через
|
||||
квартал**, не помня контекста. Оба положения наказывают одно и то же — слова, не
|
||||
несущие сведений. Информационный стиль ровно про это, и его польза здесь не
|
||||
эстетическая: текст, из которого нельзя достать факт, заставляет открывать код,
|
||||
а это и есть цена, которой мы избегаем.
|
||||
|
||||
## Что взято сверх правил вычитки
|
||||
|
||||
Эти три требования судит человек, а не проход вычитки: находка по ним требует
|
||||
увидеть текст целиком, а не фразу.
|
||||
|
||||
**Полезное действие.** У каждого текста есть вопрос, на который он отвечает, и
|
||||
читатель, который этот вопрос задаёт. Не отвечает — не пишется. У задачи это
|
||||
«зачем она нужна» и «что станет наблюдаемо иначе»; у документа канона — его
|
||||
собственный вопрос («что это за система», «как сложено», «почему так решили»).
|
||||
Текст, который не отвечает ни на чей вопрос, сокращается до нуля — это законный
|
||||
исход правки.
|
||||
|
||||
**Параллельность.** Однородное пишется одинаково: пункты списка — одной
|
||||
грамматической формой, разделы одного вида — одним порядком, заголовки одного
|
||||
уровня — одним типом фразы. Разнобой читатель принимает за разницу по существу и
|
||||
ищет её.
|
||||
|
||||
**Заголовок работает.** Заголовок называет содержание раздела, а не тему
|
||||
вообще: «Что проверяет `check`», а не «О проверках». Заголовков ставится
|
||||
столько, чтобы длинный текст можно было просматривать, а не только читать
|
||||
подряд.
|
||||
|
||||
## Что отброшено намеренно
|
||||
|
||||
Инфостиль написан для текстов, где читателя надо удержать. Проектный текст
|
||||
читают потому, что надо, и держать его нечем. Отсюда три расхождения:
|
||||
|
||||
- **Парцелляция и рубленые фразы — нет.** Приём «Коротко. Ещё короче. Вот так»
|
||||
ломает причинную связь, а в решении и в задаче ценность именно в ней:
|
||||
«поэтому», «иначе», «раз так» несут смысл и остаются.
|
||||
- **Не всякое вводное — мусор.** «Если», «иначе», «при таком-то условии»,
|
||||
«в отличие от» — это условия и противопоставления, то есть сведения. Режутся
|
||||
вводные, которые не меняют смысл предложения.
|
||||
- **Скобки и точка с запятой остаются.** В технической записи скобки несут
|
||||
уточнение — имя команды, единицы, слаг, — и запрет на них удлинил бы текст, а
|
||||
не сократил. Запрет на многоточие принимаем: в проектном тексте оно значит
|
||||
«дописать позже», и такой текст лучше не публиковать.
|
||||
|
||||
И общее: инфостиль призывает «снять корону с себя и надеть на читателя». Здесь
|
||||
читатель — **ты сам через квартал** и тот, кто возьмёт задачу. Писать для них
|
||||
значит называть состояние и остаток, а не пересказывать, как было интересно
|
||||
разбираться.
|
||||
|
||||
<!-- /дом: язык-доктрина -->
|
||||
|
||||
## Правила
|
||||
|
||||
<!-- дом: язык-правила -->
|
||||
|
||||
У каждого правила названа причина: она же говорит, где правило **не**
|
||||
применяется.
|
||||
|
||||
1. **Глагол вместо отглагольного существительного, активный залог.** «Обработчик
|
||||
не проверяет владельца», а не «проверка владельца не осуществляется»;
|
||||
«скрипт переписывает индекс», а не «индекс переписывается скриптом».
|
||||
Отглагольное существительное прячет того, кто действует, — а в техническом
|
||||
тексте важен именно он. Страдательный залог **остаётся**, когда деятель
|
||||
неизвестен или неважен: «файл удаляется» верно, если удаляет любая из трёх
|
||||
команд.
|
||||
|
||||
2. **Факт вместо оценки.** «Время ответа доходит до 800 мс», а не «работает
|
||||
медленно»; «тело 40 МиБ держит блокировку 5 секунд», а не «большие тела
|
||||
тормозят». Оценка допустима, когда факт стоит рядом, в той же фразе. Без
|
||||
факта это настроение, а не сведение, — и находка тем ценнее, что оценку
|
||||
потом не проверить.
|
||||
|
||||
3. **Стоп-слова.** Канцелярит (является, осуществляется, в целях, в рамках,
|
||||
данный, вышеуказанный), вводные-паразиты (в общем, как известно, стоит
|
||||
отметить), усилители (очень, крайне, достаточно, абсолютно, максимально),
|
||||
синонимы одного качества («понятный и простой»), неопределённое
|
||||
(соответствующий, определённый, некоторый).
|
||||
|
||||
Проверка одна: **вычеркни слово — смысл изменился, оставляй.** И осторожно с
|
||||
вводными: «если», «иначе», «при таком-то условии», «в отличие от» несут
|
||||
условие и противопоставление, то есть сведения, — их не трогают.
|
||||
|
||||
4. **Одна мысль — одно предложение.** Предложение с двумя независимыми
|
||||
утверждениями делится. **Причинную связь не режут**: «поэтому», «иначе», «раз
|
||||
так» — смысл, а не длина; рубленые фразы ради краткости тут вредят.
|
||||
|
||||
**Поля меты не делятся.** «Зачем» в мете задачи по формату — одно
|
||||
предложение: оно повторяется строкой индекса, и второму там не поместиться.
|
||||
Тесно — сокращают, но не делят. То же с любым полем вида `- **Имя:** …`.
|
||||
|
||||
5. **Англицизм, у которого есть живое русское слово, заменяется.**
|
||||
|
||||
| Калька | Русский аналог |
|
||||
| --- | --- |
|
||||
| флоу | поток, процесс, сценарий |
|
||||
| фикс, зафиксить | исправление, исправить, починить |
|
||||
| чекать | проверять |
|
||||
| апрув, заапрувить | согласование, согласовать |
|
||||
| best-effort | по возможности |
|
||||
| кейс | случай, сценарий |
|
||||
| перформанс | производительность |
|
||||
| матчинг, смэтчить | сопоставление, сопоставить |
|
||||
| зарелизить | выпустить, выложить |
|
||||
| отрефакторить | переписать, разделить, убрать второй путь |
|
||||
|
||||
Насильно не переводится то, что является **именем вещи**: термины технологий
|
||||
и протоколов (`SQL`, `API`, `CSV`, `N+1`, `IDOR`), имена классов, методов,
|
||||
полей, таблиц и команд, слаг, а также термин, у которого нет точного русского
|
||||
эквивалента и который в команде уже прижился.
|
||||
|
||||
Цель — простой и точный текст, а не пуризм. Русский аналог звучит коряво или
|
||||
искажает смысл — остаётся термин.
|
||||
|
||||
6. **Слово из своего словаря не трогается — список закрыт.** Оговорка «термин
|
||||
прижился» без списка проверяема на глаз и потому не проверяема: прижившимся
|
||||
выглядит любое слово, встреченное трижды.
|
||||
|
||||
| Термин | Что называет |
|
||||
| --- | --- |
|
||||
| интейк | заведение записи с фильтром и дедупом: «заведение» называет создание файла, слить их — смешать две операции |
|
||||
| триаж | стадия конвейера, сводящая находки в решение |
|
||||
| провенанс | обязательное свойство числа: чем и при каких условиях получено. «Источник» рядом называет саму запись, а не свойство |
|
||||
| дедуп, дедупликация | сверка нового против уже лежащего |
|
||||
| чек-лист | перечень, по которому идут сверху вниз, называя исход каждой строки |
|
||||
| дифф, `--base` | разница между состояниями в git |
|
||||
| промпт | текст, которым зовут модель |
|
||||
| change, capability, spec | сущности OpenSpec, имена вещей чужого инструмента |
|
||||
| generative, applicative | роды проходов ревью, вводятся определением по месту |
|
||||
|
||||
**Список закрыт.** Слово не отсюда и не из таблицы имён вещей выше — находка,
|
||||
а не «принятый стиль»: у него либо есть живой русский аналог, либо оно
|
||||
требует ввода одной строкой при первом употреблении.
|
||||
|
||||
Отсюда же читается снятое. Эти слова из текстов убраны, и возвращать их не
|
||||
надо: **конфляция** (смешение), **декорреляция** (разведённость, разведён с
|
||||
кем-то), **непоймание** (почему не поймали), **эвал-сет** (проверочный
|
||||
набор), **гайд** (руководство). Каждое было латинизмом или калькой при живом
|
||||
русском слове, и каждое к моменту снятия жило в трёх-шести файлах разом — то
|
||||
есть выглядело словарём, не будучи им.
|
||||
|
||||
7. **Жаргон и метафоры заменяются прямым называнием.** Автору образ понятен,
|
||||
читателю — нет.
|
||||
|
||||
| Метафора-жаргон | Прямо |
|
||||
| --- | --- |
|
||||
| рычаг (кэша, отбора) | условие отбора, параметр |
|
||||
| навешен не на тот счётчик | завязан не на тот счётчик |
|
||||
| переширокий матчинг по имени | слишком грубое сопоставление по имени, слишком много слабых совпадений |
|
||||
| костыль | временное решение, обходной путь — и в чём именно |
|
||||
| просело, отвалилось | стало медленнее на столько-то, перестало отвечать |
|
||||
|
||||
Проверка: **фраза требует, чтобы читатель додумал образ, — заменяется
|
||||
буквальным описанием того, что происходит.**
|
||||
|
||||
8. **Термин, которого нет в документах проекта, вводится одной строкой или не
|
||||
употребляется.** Термин, не встречающийся ни в паспорте, ни в архитектуре, ни
|
||||
в конвенциях, — свой словарь у отдельной записи, а это самый дешёвый способ
|
||||
сделать беклог нечитаемым для того, кто вернётся к нему через квартал.
|
||||
Заменять незнакомый термин догадкой нельзя: догадка о предметной области
|
||||
дороже непонятного слова, потому что выглядит понятной.
|
||||
|
||||
**Слово, занятое в другом смысле, — то же нарушение.** Термин, который в
|
||||
одном документе проекта значит одно, а здесь другое, ломает оба.
|
||||
|
||||
9. **Имя файла — английское слово по сути, а не транслит.** `queue-as-table`, а
|
||||
не `ochered-tablicej`; `move-parse-strict`, а не `razbor-hoda`. Транслит
|
||||
нечитаем тому, кто ищет по смыслу, и не сокращается, а имя стоит в ссылках,
|
||||
коммитах и путях, которые набирают руками. Переименование — **перенос ссылок
|
||||
одним проходом**, а не правка одного файла.
|
||||
|
||||
<!-- /дом: язык-правила -->
|
||||
|
||||
## Порог правки
|
||||
|
||||
<!-- дом: порог-правки -->
|
||||
|
||||
**Правка без нарушенного правила не делается.** Текст, переписанный «чтобы
|
||||
звучало лучше», обесценивает список замечаний: когда половина из них вкусовая,
|
||||
перестают читать весь список, и вместе с ним пропадают настоящие находки.
|
||||
Сомневаешься — не правь. Формулировка, которая просто **не твоя**, — не находка.
|
||||
|
||||
**Систематичность нарушения — не довод в его пользу.** Одна и та же ошибка в
|
||||
пяти файлах не становится «принятым стилем»: чаще это значит, что правило не
|
||||
применялось вовсе, — и находка тем важнее. «Так сделано везде» годится как
|
||||
основание для **одной находки на весь набор** («правило N нарушено в пяти
|
||||
записях, перечень: …»), но не как основание промолчать. Принятым считается
|
||||
только то, что назвал зовущий или что записано в конвенциях проекта.
|
||||
|
||||
<!-- /дом: порог-правки -->
|
||||
|
||||
И обратное: язык правится **по ходу той операции, которая записи касается**.
|
||||
Беклог не переписывают ради языка.
|
||||
|
||||
## Доклад вычитки
|
||||
|
||||
Не правило языка, а **контракт прохода**: форма, в которой находка приходит к
|
||||
человеку. Живёт здесь потому, что проходов вычитки несколько — по одному на
|
||||
плагин, — и разойтись формой они не должны.
|
||||
|
||||
<!-- дом: вычитка-доклад -->
|
||||
|
||||
Находки по одной, в порядке важности: залог и оценки → жаргон и англицизмы →
|
||||
стоп-слова. Первые меняют, **что** читатель понимает; последние — только сколько
|
||||
он на это тратит.
|
||||
|
||||
```
|
||||
<файл>
|
||||
правило: <номер и короткое имя>
|
||||
сейчас: <как написано>
|
||||
предложение: <готовая формулировка, подставляемая как есть>
|
||||
почему: <одна фраза>
|
||||
```
|
||||
|
||||
В конце — **границы покрытия**: сколько файлов просмотрено из скольких, какие не
|
||||
смотрел и почему, и по чему проверялись термины (документы проекта названы или
|
||||
нет). Отчёт без этой строки читается как «всё вычитано», не сообщая, какая часть
|
||||
осталась нетронутой. Туда же — строка «замечено не по моей части»; машинно
|
||||
проверяемое в неё **не идёт**.
|
||||
|
||||
Ничего не нашёл — так и скажи одной строкой. Пустой доклад с границами покрытия
|
||||
полезнее выдуманной находки.
|
||||
|
||||
<!-- /дом: вычитка-доклад -->
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
# Сопровождение и эксплуатация
|
||||
|
||||
**Это дом.** Словарь «чем держат проект» назван в трёх местах трёх разных
|
||||
плагинов: секция `Сопровождение` в роадмапе (`av-dev-tasks`), раздел
|
||||
«Эксплуатация» в `architecture.md` (`av-dev-docs`) и тема ревью `operations`
|
||||
(`av-dev-code`). Ни один из трёх им не владеет, поэтому дом стоит снаружи, а
|
||||
плагины везут копии.
|
||||
|
||||
Три перечня «чем держат проект» уже разъезжались — на «метриках и логах» против
|
||||
«мониторинга», — и разъехались молча. Отсюда дословная копия вместо ссылки.
|
||||
|
||||
<!-- дом: сопровождение-словарь -->
|
||||
|
||||
Одна тема живёт в трёх местах, и путать их слова нельзя.
|
||||
|
||||
**Сопровождение** — всё, чем держат проект: инструмент и сборка, процесс,
|
||||
выкладка, метрики и логи, инфраструктура, дежурство. **Эксплуатация** — его
|
||||
часть: работа системы на проде. Целое и часть, и никогда наоборот.
|
||||
|
||||
| Место | Уровень | Что там |
|
||||
| --- | --- | --- |
|
||||
| `ROADMAP.md`, секция `Сопровождение` | план | **работы**, которые собираемся делать: цели и их задачи |
|
||||
| `architecture.md`, раздел «Эксплуатация» | состояние | **как устроено сейчас**: где работает, что рядом, кто перезапускает |
|
||||
| тема ревью `operations` | оптика | **чем проверяем**: «это упало через неделю на проде» |
|
||||
|
||||
Слово **«поддержка» не употребляется вовсе** — в нём слышится помощь
|
||||
пользователю, а это другая работа.
|
||||
|
||||
**Граница с возможностями проходит по тому, кто наблюдает.** «Приложение
|
||||
сообщает о своём состоянии» — возможность приложения, её место среди прочих
|
||||
целей: наблюдает пользователь сервиса. «Дежурный видит состояние на одном
|
||||
экране» — сопровождение: наблюдаем мы. Одни и те же метрики попадают в разные
|
||||
секции роадмапа, и это верно — секции отвечают на разные вопросы.
|
||||
|
||||
<!-- /дом: сопровождение-словарь -->
|
||||
@@ -0,0 +1,57 @@
|
||||
# Граница между плагинами
|
||||
|
||||
**Это дом.** Правило обращения к соседнему плагину нужно всем, кто зовёт чужой
|
||||
скилл, — а таких скиллов больше половины всех, и ни один плагин правилом не
|
||||
владеет. (Числа здесь нет намеренно: оно уже дважды протухало за один день.) Поэтому дом стоит снаружи, а плагины везут **копии**, помеченные
|
||||
разметкой `copies.py`.
|
||||
|
||||
Правится **здесь**. Копия, поправленная у себя, — расхождение, а не правка.
|
||||
|
||||
Дом заведён по замеру, а не на всякий случай. К моменту раскола правило стояло в
|
||||
пяти местах в пяти редакциях:
|
||||
|
||||
| Где стояло | Довод | Ветка «не разрешился» |
|
||||
| --- | --- | --- |
|
||||
| `task-pipeline` | устаревшая проектная копия | нет |
|
||||
| `task-batch` | то же | нет |
|
||||
| `review-pipeline` | вшито в пункт про удаление проектных копий | нет |
|
||||
| `openspec` | путём в чужое дерево — никогда | есть |
|
||||
| `canon` | — | есть |
|
||||
|
||||
Имена с тех пор изменились — `task-pipeline` стал `resolve`, `review-pipeline` —
|
||||
`review`, `task-batch` удалён, — но замер относится к местам, а не к названиям.
|
||||
|
||||
Два разных довода, и ни в одном месте не было обоих. Три места из пяти молчали о
|
||||
том, что делать, когда вызов не разрешился, — то есть о единственном, ради чего
|
||||
правило и написано.
|
||||
|
||||
**Что в дом не идёт: чем оборачивается отсутствие конкретного соседа.** «Нет
|
||||
`av-dev-tasks` — учёт остаётся владельцу» знает только конвейер; «нет конвейера —
|
||||
`docs.py` о каталоге `openspec/` молчит» знает только канон. Правило общее,
|
||||
последствие местное, и держать последствия здесь значило бы завести дом, который
|
||||
знает про всех своих потребителей.
|
||||
|
||||
<!-- дом: граница-плагинов -->
|
||||
|
||||
Плагины `av-dev` ставятся порознь, и ни один не вправе считать, что сосед на
|
||||
месте.
|
||||
|
||||
**Чужой скилл зовётся полным именем** — `av-dev-docs:canon`, `av-dev-tasks:tasks`,
|
||||
`av-dev-code:review`. Короткое имя может разрешиться в устаревшую проектную копию
|
||||
из `.claude/skills/`, и подмены не будет видно ни в докладе, ни в поведении.
|
||||
|
||||
**Путь в дерево чужого плагина не пишется никогда.** `$CLAUDE_PLUGIN_ROOT` ведёт
|
||||
только в свой плагин; вычисленный от него путь к соседу либо не откроется, либо
|
||||
откроет чужую установку. Нужен чужой справочник — зови владеющий им скилл, он
|
||||
прочитает его сам.
|
||||
|
||||
**Вызов не разрешился — плагина в проекте нет.** Это исход, а не поломка: назови
|
||||
строкой доклада, чего теперь не делает никто, и продолжай работу. Молчать нельзя,
|
||||
пропуск неотличим от сделанного; выдумывать обходной путь нельзя тоже.
|
||||
|
||||
**Присутствие узнаётся вызовом или следом в проекте, но не объявлением.** Перечня
|
||||
установленных плагинов проект не ведёт — он разошёлся бы с действительностью
|
||||
молча. Что сосед здесь работал, видно по заведённому им файлу: `docs/.pm.json` —
|
||||
канон, `<каталог задач>/.tasks.json` — задачи, `openspec/config.yaml` — конвейер.
|
||||
|
||||
<!-- /дом: граница-плагинов -->
|
||||
Reference in New Issue
Block a user