Compare commits

...
91 Commits
Author SHA1 Message Date
av 7ab759ae4a старший долг: развилка тремя основаниями, вопросы тем, версия раскладки 5
Третий заход по находкам ревью — то, что старше темы 78 и тянулось с тем 74–77.

Оснований у развилки три во всех местах: конвейер называл два, а устав триажа,
контракт находок, сценарий решения и журнал — три. Там же сказано, чем третье
отличается: по первым двум оркестратор урезает изменение до остатка, третье
отменяет одобрение и возвращает на чекпоинт.

Вопросы проекта по темам достались проходам, которые эти темы закрывают:
review-code, review-specs и review-autotests получили обязанность отвечать
дословно и строку в блоке покрытия. Прежде конвейер обещал их каждому проходу,
а знал о них только приёмник тем.

Глубокое ревью приведено к уставам, которые зовёт: глубина у проходов разная —
доказательство у тех двоих, что держат машину, разбор у architecture и code;
у триажа три вызывающих, а не два режима, и потолка в 7 пунктов там нет.

Версия раскладки поднята до 5 с записью журнала: скелет docs/review.md потерял
подраздел «Триггеры метки» ещё темой 77, а миграции проектам никто не дал.
Сняты остатки меток в task-track и в config-skeleton, уезжающем в чужой проект.
Перечень осей досчитал три оси: глубина темы, разметка действия, род правки.

Журнал — тема 81.
2026-08-23 19:51:47 +03:00
av d79f9d2286 хвост: учёт зовёт оркестратор, третий такт идёт и на отказе
Ревью трансформации нашло, что тема 78 спорит сама с собой в четырёх местах.

Учёт был отдан агенту, хотя перечень оркестратора объявлен закрытым, а
границы задания прямо говорят «задач не заводит»: вызов av-dev:task-track
вернулся оркестратору, агенту третьего такта осталось письмо в документы.

Ветка отказа не была покрыта — на ответе «ничего» правка первого такта
уезжала в коммит невычитанной и с непрогнанным гейтом. Теперь третий такт
идёт всякий раз, когда была реплика; не идёт он только тогда, когда реплики
не было вовсе. Дом правила вычитки в doc-sync знает про два захода.

Барьер карты кластеров из сценария «задачи из ревью и аудита» снимается там,
где его уже прошли: список показан человеку и получил ответ. При прямом
вызове и вызове из code-deep-review карта по-прежнему вопрос.

Счёт стопов сведён в таблицу по сценариям; у обслуживания появился второй
заход и одна реплика с поводом «новый запрет или инвариант». Сигнал сверки
считается по архиву change и каталогу задач разом — иначе chore и research
не считались вовсе — и вошёл в возврат агента и в доклады трёх сценариев.
Ось «род правки документа» внесена в перечень осей.

Журнал — тема 80; отложенный старший долг назван в С287.
2026-08-23 19:43:49 +03:00
av 8145b378b2 след сверки: ключ переехал в секцию docs, конфиг больше не отвергается
Ключ [healthcheck] last, заведённый вчера темой 78, роняли оба скрипта
плагина: верхний уровень .av-dev.toml стережёт TOP_KEYS в shared/config.py,
и неизвестный ключ там — отказ кодом 3. Первый же прогон сверки сделал бы
нерабочими canon check/adopt/upgrade, весь task-track и гейт проекта.

След живёт теперь ключом healthcheck_last в секции [docs] — по смыслу
(настройки проверок документов) и по цене (правка одной константы DOCS_KEYS
в docs.py, общий читатель не тронут). Воспроизведено до и после: docs.py и
tasks.py конфиг с ключом принимают.

Корень был не в описке, а в ложном обещании канона «неизвестный ключ docs.py
игнорирует» — оно старше темы 78, и на него опёрлись, не открыв скрипт.
Обещание снято, на его месте правило: новый ключ заводится правкой константы
скрипта-владельца, и только потом попадает в канон.

Журнал — тема 79.
2026-08-23 19:35:17 +03:00
av a4bc9191e7 хвост задачи: отражение молча, новое — по слову человека
Синк документации делил правки по документам, а делить их надо по роду.
Отражение сделанного (вливание дельт, миграция, компонент в обзоре) пишется
молча: без правки документ станет ложным. Новая запись и новая норма — ADR,
конвенция, записка разведки, инвариант, периметр, дефект в журнале — только
предлагаются, а пишет их третий такт шага 6 после слова человека.

Реплика при этом одна на весь хвост: вопрос про урожай ревью переехал с шага 5
на шаг 6 и слился с предложениями синка — решение одно, «что из найденного
переживёт задачу». Плановых стопов в сценарии решения стало ровно два, и оба
про решения человека.

Сверка документов получила счётчик: doc-healthcheck оставляет след ключом
[healthcheck] last в .av-dev.toml, синк считает по нему задачи с прошлого
прогона и говорит строкой. Прежде признак «десяток задач» держался в памяти,
то есть не срабатывал.

Журнал — тема 78.
2026-08-23 19:06:06 +03:00
av 3c89d7111d ревью: цикл задачи проверяет механику, метки сняты
Состав прогона постоянный: гейт, спеки, код, триаж; приёмник тем идёт,
когда у проекта есть свои темы. Метка, разметка и проход review-scope
упразднены, review-levels.md удалён, ось «метка» снята из axes.md.

Ступень 4 ушла из цикла: review-proof упразднён через день после
заведения, review-architecture переехал в code-deep-review вслед за
adversary и ops. Темы security, operations и architecture закрывает
review-code сверкой с записанными инвариантами, потолком 1 находка.

Умолчание разметки действий перевёрнуто на инлайн; развилка осталась
за необратимым, изменением дельта-спек и нарушенным инвариантом.
Задачи из урожая заводятся по слову человека, а не шагом сценария.

Чекпоинт назван единственным местом, где решается форма решения.
Потеряны ось времени в цикле и суждение о форме после кода — обе
потери названы в «Честном пределе» строкой границ покрытия.

Журнал — тема 77.
2026-08-23 17:26:07 +03:00
av daf9f8b824 ревью: лёгкий проход proof в цикле, тяжёлые — в code-deep-review
В цикле задачи темы security и operations закрывает один лёгкий проход
review-proof: чтением и рассуждением, без запуска, потолки раздельные. Машину
он не держит, поэтому идёт в общем залпе — цепочки за ресурс в обычном прогоне
не осталось. Тяжёлая пара adversary и ops переехала в новый скилл
code-deep-review: вход — названная область кода, глубина постоянная, исход —
разбор с человеком и задачи через task-track. Вход глубокому прогону копит сам
цикл строками «отложено». Журнал — тема 76.
2026-08-23 15:55:32 +03:00
av b287cdf71f resolve: хвост задачи собран в один агентский запуск
Архивация change и синк документации уходят одному агенту одним заданием:
второй шаг читает то, что оставил первый, и платить дважды за сбор того же
контекста незачем. Гейт после правок документов доводит тот же агент, вычитку
языка зовёт сам doc-sync. Коммит и закрытие задачи остаются оркестратору —
они необратимы для учёта. В обслуживании синк тоже ушёл агенту. Журнал —
тема 75.
2026-08-23 15:29:03 +03:00
av 72d9aa8034 resolve: ревью дизайна снято, разметка переехала за код
Сценарий решения идёт от предложения сразу к чекпоинту и коду: стадия ревью
дизайна упразднена целиком, review-scope запускается после apply и меряет
размер по диффу, сложность — сверкой обещанных границ с тронутыми. Чекпоинт
остался единственным плановым стопом и стоит теперь до кода. review-rubric
конвейером не зовётся, слот рубрики в скелете config.yaml снят. Журнал —
тема 74.
2026-08-23 15:08:17 +03:00
av 17be316634 ревью: гейт, прогнанный до ревью, засчитывается по отпечатку дерева
Повтор той же команды на неизменившемся дереве снят: ступень автотестов
засчитывает прогон, сделанный шагом opsx:apply или шагом гейта обслуживания.
Признак — отпечаток рабочего дерева, снятый дважды; любое расхождение ведёт
к честному прогону. Журнал — тема 73.
2026-08-23 13:42:58 +03:00
av 813345192d resolve: письмо вынесено агентам, журнал — тема 72
Спеки, код и правки по находкам ревью пишет отдельный агент: оркестратору
оставлены задание, возврат, чекпоинт, сверка плана с исходом и доклад.
Заведён раздел «Кто пишет» в SKILL.md, переписаны шаги 2, 4, 6, 7 решения и
шаги 2, 4 обслуживания, у разведки названо, почему исполнителей нет.
2026-08-23 07:56:15 +03:00
av e5dc0a1a39 язык: сняты «провенанс» и «интейк», назван образец стиля
Оба слова стояли в закрытом словаре правила 6 с оговоркой, и обе оговорки
отвергали один русский вариант, а вывод из них делался про все. Отсюда общее
требование к записи словаря: она обязана говорить, чем слово незаменимо, а не
чем плох один из кандидатов. Латинизм, переживший проверку одним синонимом, —
не имя вещи, а непроверенная привычка.

Провенанс заменён двумя словами, потому что смысла было два, и это же его и
держало: происхождение у числа (чем и при каких условиях получено) и откуда у
вопроса и находки (кто нашёл, каким проходом, из какой записи журнала). Слово
стояло и в скелете docs/review.md, уезжающем в репозитории проектов, поэтому
раскладка повышена до версии 4 с записью журнала: правка формы вопроса и
проход grep по docs/.

Интейк заменён заведением с названным источником — «из диалога», «из ревью».
Оговорка защищала слово от голого «заведения» и в этом была права, но в паре с
источником двусмысленности нет, а скилл задач уже называет операцию так же.
Раскладку это не двигает: слово жило только в прозе плагина.

Образец стиля назван прямо и отдельным разделом: научно-популярная книга, не
спецификация и не конспект для себя. Три умолчания — воды нет, сложных
конструкций нет, англицизм исключение с причиной. Находок образец не
порождает: он для того, кто пишет, а вычитка судит по правилам, иначе «звучит
сложно» стало бы находкой и порог правки перестал бы работать.

Журнал решений: темы 70 и 71, Р258–Р264 и С244–С249. Остальной словарь —
триаж, дедуп, чек-лист, дифф, промпт, чекпоинт, синк — не пересматривался, и
это сказано записью: пересмотр меняет язык всего корпуса и делается своей
работой, а не попутно.
2026-08-13 19:43:08 +03:00
av e78a4311a7 журнал решений: тема 69 — счёт корпуса в прозе не пишется
Р253–Р257 и С241–С243: почему число, называющее размер корпуса, расходится
молча; какие два способа сослаться не стареют; почему число-заголовок к
перечню и замер с провенансом правилом не задеты; почему дом правила —
язык проектных текстов, а не канон.

Указатель журнала перестал перечислять занятые номера: диапазон устаревал
на каждой теме и требовал правки в файле, которого тема не касается.
2026-08-13 19:30:13 +03:00
av dd7aa22d02 язык: счёт корпуса в прозе запрещён правилом 10
«Пять ревью», «три capability», «десять проходов» читаются как сведение, а
живут до ближайшего пополнения корпуса. Расхождение молчаливое вдвойне:
фраза остаётся грамматически исправной, диффом не ловится — правят не её, а
корпус, — и проверяется только пересчётом, которого никто не делает.

Сослаться можно двумя способами, и оба не стареют: на конкретную запись
именем, слагом или датой либо на корпус целиком. Величину называет сам
каталог в момент чтения, а абзац её только запоминает.

Две границы названы явно, иначе правило запретило бы форму, на которой
держится половина процессных текстов. Число-заголовок к перечню,
приведённому тут же, не задето: правят его в той же строке, что и список.
Замер с провенансом не задет тоже — он про прошлое и не пополняется.
Разделяет вопрос «изменится ли число само, без правки текста».

Судит вычитка, пофразно: правило уехало помеченной копией в уставы
doc-wording и task-wording, у обоих названы частое место находки и запрет
пересчитывать корпус — находка в самом числе, а не в его неверности.
Описания агентов дополнены, чтобы не отстать от механики. В карте домов
канона стоит ссылка: счёт корпуса выглядит не копией, а собственным
наблюдением документа.

Собственная проза приведена к правилу: девять правил языка (их стало
десять этим же коммитом), десять агентов-проходов и девять скиллов в
README, восемь скриптов в перечне осей, шесть тем ядра в сценарии решения
и в конвейере ревью, семь проходов в правилах нарезки.
2026-08-13 19:30:01 +03:00
av 77cb967d7b журнал решений: тема 68 — постановка текстом
Р245–Р252 и С237–С240: почему форм постановки две и почему форма — не
четвёртый сценарий; почему отпадают ровно ready и закрытие; почему запись
не заводится задним числом; почему названный вслух до работы тип
работает признаком, а названный после — уже нет; где решению разрешено
предложить себе критерии приёмки и почему обслуживанию — нет.
2026-08-13 19:20:36 +03:00
av 94fa66b262 resolve: постановку текстом взяли полноправным входом
Задачу часто нужно решить прямо по описанию в разговоре, без файла в
каталоге — так её берёт и opsx:propose. Скилл вход текстом объявлял, но
прорабатывала его одна разведка: у решения и обслуживания шаг «прочитать
задачу» читал разделы записи, шаг закрытия закрывал запись, признак
обслуживания опирался на объявленный автором тип, а критерии приходили
«от проекта».

Форм постановки теперь две, и они равноправны. Отпадают ровно те шаги, у
которых пропал предмет: ready гонять нечего, закрывать нечего. Ни один
шаг с предметом не выпал — гейт, ревью, синк, чекпоинт и коммит идут как
обычно, а сценарий, метку и глубину форма не выбирает.

Взамен пропавшего — названное вслух первой репликой: как понята
постановка, каким типом её считаешь и где проводишь границу. Человек,
написавший текст, сидит в этом же разговоре и поправляет одной фразой;
названный после работы тип не признак, а объяснение готового диффа.

По сценариям: решение добирает недостающие критерии приёмки на чекпоинте
и считает их данными только после ответа; обслуживание объявляет их
отсутствие строкой (чекпоинта у него нет) и само называет границы,
которых текст не дал; разведка увязана с общим правилом, а её шаг
закрытия отпал с оговоркой про единственный след — записанный ответ.

Записи в каталог скилл по-прежнему не заводит: ни перед работой, ни
задним числом ради закрытия. Похожую строку беклога не разыскивает.

Перечень осей пополнен формой постановки: по ней ветвятся готовность,
источник типа и наличие закрытия.
2026-08-13 19:20:25 +03:00
av 6251157d8d журнал решений: тема 67 дополнена по итогам ревью
- снята выдуманная ссылка: фраза «цель и приоритет — независимые оси»
  приписывалась теме 19, а стояла в правиле 4 самого скилла;
- перечень отменяемого доведён до полного: тема 19 целиком кроме Р81,
  половина Р83, плюс Р69, Р84, С85, Р101, Р103, Р105, Р106, Р107, Р110,
  Р128 из тем 17, 20, 25, 26, 27, 31;
- Р242 приведён к тому, что команда делает на самом деле; заведены Р244
  (объявление стадии и её смена — разные операции) и С234–С236.
2026-08-13 15:08:43 +03:00
av ed83ec7dc0 задачи: починена смена стадии, разобраны находки ревью плагина
Команда stage была дефектна по шести пунктам, и все шесть подтверждены
прогоном: не звала raw_last (переход оставлял каталог красным), не
переписывала шапку беклога (индекс продолжал объявлять прежнюю стадию),
шла в обход write_config, молча пропускала файлы с непересобираемой метой,
ломалась на беклоге без заголовков и схлопывала полки при первом
объявлении стадии.

Объявление и смена разведены: объявление беклога не трогает вовсе, смена
трогает состав секций только по явному --sections, а слить полки скрипт
не берётся ни в одном случае. Абзац шапки размечен парой «стадия», и
расхождение с конфигом стало обычным дрейфом.

Отказ по недостающей строке индекса запирал запись, пережившую упразднение
роадмапа: edit, close и reopen теперь заводят или пропускают строку сами.
Прочее: регистр stage нормализуется при чтении; --fix снимает мёртвые теги
и у неразобранных записей; move отказывает переставлять сырьё; adopt
держит место сырья; docs.py bump двигает одну запись журнала за раз;
tasks.py получил перечень упразднённых адресов, и гейт наконец видит
собственное упразднение ROADMAP.md.

Запись «Версия 3» переписана по прогону на игрушечном проекте: прежний
порядок шагов был неисполним. Закрыты дыры модели стадий (пересмотр плана
стройки стал сценарием, приёмка отвязана от груминга, from-review,
research и adopt получили развилку по стадии, перечень осей пересчитан) и
находки, старшие этой сессии: review-triage получил режим без метки, три
списка проектных копий сведены к дому с проверяемыми копиями, пять
пересказов правил стали помеченными копиями или ссылками, language.md
перестал объявлять юрисдикцию над чужим плагином.
2026-08-13 15:08:29 +03:00
av 8d8c1656e5 журнал решений: тема 67 — цель упразднена, у проекта появилась стадия
Р237–Р243 и С228–С233: почему цель была зонтиком над параллельными
направлениями и почему у линейного списка работ его нет; почему «Готово»
удалена, а не перенесена; почему стадия объявляется явно и почему её
русское имя — «доработка», а не занятая «поддержка».
2026-08-13 14:26:32 +03:00
av 3849f084be задачи: цель упразднена, у проекта появилась стадия
Тип goal и индекс ROADMAP.md убраны: цель — зонтик над параллельными
направлениями, а у проекта на одного человека список работ линеен. Роадмап
при этом наполовину дублировал беклог, а «что уже умеет» отвечают спеки и
git log индекса. Секция «Готово» удалена, а не перенесена.

Вместо цели — ось «стадия проекта»: build (беклог это план стройки, порядок
строк значит зависимость, секция одна) и support (очередь правок, порядок
значит важность, секции — полки домена). Стадия объявляется ключом
[tasks] stage, меняется командой stage, без неё check отказывает: порядок
строк нечем прочитать.

Ушли теги goal:/decomposed, поле «Секция», раздел «Завершение», флаги
--goal и edit --section. Версия раскладки 2 → 3, перевод проекта расписан
записью журнала.
2026-08-13 14:26:21 +03:00
av 0627199a1a журнал решений: тема 66 — скилл формы вышел из семейства документов
- Р235: префикс называет материал, а `canon` занят формой, общей у всех частей
  проекта; версия стала общей ещё на слиянии, и с того дня `doc-` спорил с
  механикой;
- Р236: переименование поехало записью журнала версий, хотя в проекте ничего
  не переехало — сломались бы путь в гейте и имя вызова;
- С226 и С227: префикс — проверяемое утверждение о владении материалом;
  переименование скилла есть изменение раскладки, если проект держит его адрес
  у себя.
2026-08-13 12:53:27 +03:00
av dff05ad097 скиллы: doc-canon стал canon, версия раскладки поднята до 2
- каталог скилла и все вызовы переименованы: префикс `doc-` называл материал,
  а скилл занят формой — раскладкой всех частей проекта и общим повышением
  версии, включая каталог задач;
- README перестроен: `canon` вынесен из семейства документов отдельным блоком
  и отдельным узлом графа, правило префиксов переформулировано, у документов
  уточнено владение — содержимым, а не раскладкой;
- заведена запись 2 журнала версий: в проекте ничего не переехало, но путь к
  `docs.py` и имя вызова живут в гейте и в `CLAUDE.md` проекта и сломаются
  молча;
- прежние адреса в записи 1 и в журнале решений оставлены как есть: журнал
  описывает состояния, которые были, и задним числом не переписывается.
2026-08-13 12:53:12 +03:00
av 3529cd8425 удалены TODO.md, REMAINING.md и HISTORY.md
- указатель в README сведён к журналу решений; из decisions/README.md убрана
  строка про остатки, из addresses.py — HISTORY.md в перечне журналов;
- упоминания этих файлов внутри журнала оставлены как есть: он описывает
  прошлые состояния и задним числом не переписывается.
2026-08-13 12:43:09 +03:00
av eae734f5cc гейт: добавлена проверка ссылок и номеров журнала решений
- `decisions.py` судит четыре вещи: уникальность номеров Т/Р/С, раскладку тем,
  указатель и ссылки — цель существует, подпись называет именно её;
- проверка идёт без glob, как адреса: файл темы и ссылки на него лежат порознь,
  и переименование темы трогает только одну сторону, а ломает обе;
- ссылка внутри блока кода ссылкой не считается — в скелетах канона она
  адресована дереву проекта.
2026-08-13 12:41:13 +03:00
av bf6a173115 журнал решений: разложен по теме на файл, метки решений стали номерами
- DECISIONS.md (4040 строк, 65 тем) → decisions/, файл на тему плюс указатель;
- буквенные метки решений заменены сквозными Р1–Р234, следствия получили
  префикс С при прежних номерах: схема букв выродилась до пятибуквенных и
  сломалась — `АЕАКЛ` была занята и темой 53, и темой 65;
- 42 перекрёстные ссылки переписаны под новые номера и стали живыми; где номер
  означал тему, а слово стояло «решение», формулировка исправлена.
2026-08-13 12:40:56 +03:00
av b411d4edb8 оси: перечень получил дом, две бездомные оси переехали в shared
Слияние ничего из идей не тронуло, но сделало дешёвым дом для правила, натянутого
между скиллами. Заведён shared/axes.md — дом перечня, а не значений: девять осей,
их адреса и чего каждая не решает. Механика остаётся у владельца.

Целиком сюда переехали две оси, у которых владельца не было. Коды выхода
объявлялись общим словарём в одиннадцати местах, и каждое объявление называло
свой набор соседей; машина их не сверяла, потому что copies.py смотрит markdown,
а перечни лежали в docstring'ах. Теперь дом один, скрипты держат указатель, а три
SKILL.md — помеченную копию, потому что на кодах они ветвятся. Режим прогона
(с меткой, без метки) был размазан по четырём файлам и осью назван не был, хотя
в уставе review-basics задаёт саму возможность запуска.

Разведены два значения слова «стадия»: ступени 1-5 внутри прогона кода, стадии
дизайна и кода снаружи.

Карта нашла ошибку в себе: клетка «категория документа × метка» пустой не была —
review-basics приёмник проектных тем при любой метке. Пустой оказалась соседняя:
на прогоне без метки план фиксирован, и своих тем проекта в нём нет вовсе.
Обе оставшиеся пустоты названы вслух, а не заполнены наугад.
2026-08-13 12:16:38 +03:00
av 7333953b1e переезд: запись журнала не знала про собственные чтения конфига
Первый живой прогон на transcriber. Проект читал docs/.docs.json своим шагом
гейта — и по хорошей причине, названной там же в комментарии: чтобы не заводить
каталогу миграций второй дом. Запись 1 такого шага не предусматривала, и переезд
уронил гейт на шаге, к плагину отношения не имеющем. Появился шаг 7: найти
grep-ом свои чтения служебного файла и перевести их на tomllib.

Заодно: заключительный абзац openspec.py после вычитки ревью стал утверждать, что
документов канона в проекте нет, — печатается-то он всегда. Вернулось условие.
2026-08-13 11:57:33 +03:00
av 441469d78d вычитка ревью: пережитки трёх плагинов и язык слияния
Восемнадцать веток «плагина нет» описывали недостижимое: скиллы и агенты теперь
в одном плагине и разрешаются всегда. Где предмет всё же может отсутствовать —
ветка переписана на след в проекте (нет docs/, нет каталога задач, нет
openspec/); где отсутствовать нечему — снята. Туда же анонсы, обещавшие ветку,
которой в разделе больше нет.

Правило копий и его применение разъезжались в одном коммите: правило называло
два законных случая, а absence.md разослан семью копиями по SKILL.md. Назван
третий случай, и разрез проверяемый — файл, который модель получает целиком,
против файла, за которым она идёт отдельным чтением. Заодно сняты объявления
копий там, где копию сменила ссылка, и довод у карты домов в doc-consistency:
он ссылался на отсутствие плагина, хотя устав едет вместе с плагином.

Описания скиллов во фронтматтерах звали снятые короткие имена — по ним скилл не
находится. task-track перестал обещать повышение: версию двигает doc-canon.

Язык: сняты кросс-вызов, опцион и деградация, конверсия и «читатель» в
config.py, charter'ы против уставов, замер против подсчёта, страдательный залог
в журнале. Строка «настройки av-dev» в таблице отсутствия — слово «раскладка»
называло и целое, и его часть.

Мелкое: тема 52 в README была 64, транслит в task-wording машина не проверяет,
мёртвая ветка REQUIRED в addresses.py, ссылки на язык в закрытом журнале.
2026-08-13 11:03:11 +03:00
av 423f9798ef скрипты: запись настроек перестала уходить мимо и молчать
Найдено ревью, каждое воспроизведено на фикстуре.

Граница репозитория. find_root обходил всех предков в поисках .av-dev.toml и
только потом смотрел на .git: вложенный проект объявлялся здоровым по конфигу
соседа, а init дописывал секцию в чужой репозиторий. Подъём останавливается на
первом .git. Туда же ключ [tasks] dir: он не судился как путь, и «../соседний»
уводил запись за пределы репозитория с зелёным кодом.

Запись TOML. Значения склеивались в кавычки без экранирования — имя индекса с
кавычкой ломало файл целиком, и оба скрипта после этого отвечали кодом 3 на
любую команду. Заголовок секции искался точным сравнением строки, так что
«[tasks]  # комментарий» — ровно та возможность, ради которой взят формат, — не
находился, и дописывалась вторая таблица. set_version правил version внутри
секции и ломал файл, если версия в кавычках.

Молчание вместо отказа. Неизвестные ключи не отвергались: migrations, положенный
верхним уровнем (а именно так его перенесут руками из .docs.json), давал
«проверка неприменима» и зелёный итог. Неверный dir проваливался в поиск вверх, и
скрипт работал в другом каталоге, не сказав о ключе. Прежние файлы узнавались
только когда нового нет — половина переезда проходила молча.

Честность доклада. Версию не двигал никто: set_version был написан и не подключён,
init печатал версию, которой не записал, adopt писал dir мимо --target. Теперь
повышение — команда docs.py bump, а init и adopt зовут общий write_config, который
пишет версию, дописывает ключи и вслух называет разошедшиеся. Отсутствие
shared/config.py давало трейсбек и код 1 вместо 3. Число зовётся LAYOUT_VERSION в
обоих скриптах вместо CANON_VERSION и FORMAT_VERSION.
2026-08-13 10:56:49 +03:00
av 7d559e60ec вычитка слияния: имена скиллов в прозе, роли вместо плагинов
Короткие имена скиллов (tasks, canon, docs, groom, healthcheck) в прозе и в
уставах агентов заменены новыми: по прежнему имени скилл не находится. Фразы
вида «плагин задач», «плагина конвейера нет» переписаны на то, чем они были на
деле, — на часть раскладки проекта либо на скилл-владелец.
2026-08-13 10:35:33 +03:00
av a00e132f29 документация: README, TODO и журнал решений знают про один плагин
README переписан под два плагина: состав скиллов с префиксами, диаграмма одним
контуром, команды установки и обновления, раздел версий про .av-dev.toml. Правка
про копии сказала главное — копия была платой за неразрешимый путь, а не за
важность правила, и внутри одного дерева остаётся только там, где текст обязан
лежать в промпте.

DECISIONS: тема 64 с замером цены раскола и четырьмя следствиями. Довод «а вдруг
понадобится» снят наблюдением: режим, ради которого раскол держали, покрыт
сценарием обслуживания.
2026-08-13 10:33:44 +03:00
av 95c9499f06 конфиг: одна версия и один служебный файл, .av-dev.toml в корне
Версий было две — канон 14 в docs/.docs.json и формат задач 1 в
<каталог задач>/.tasks.json, — и порознь они двигались потому, что плагины
ставились порознь. Плагин один, версия одна и начинается с 1; журналы обеих
прежних нумераций закрыты и лежат рядом непереписанными, действующий журнал
открывается записью о слиянии с перечнем шагов проекту.

Формат TOML взят ради комментариев: файл живёт в репозитории проекта, и
назначение числа читают из него самого. Отсюда правило записи — скрипты правят
строку, а не переписывают файл. Читатель общий, shared/config.py: два разбора
одной схемы были бы двумя домами.

Каталог задач перестал узнаваться служебным файлом и называется ключом
[tasks] dir; узнают его по индексу. Прежние файлы не читаются — увидев их,
docs.py и tasks.py называют прежнюю раскладку и зовут upgrade.
2026-08-13 10:30:18 +03:00
av 6b162c421d граница: правило стало про раскладку проекта, а не про соседний плагин
Дом shared/plugin-boundary.md переехал в shared/absence.md: отсутствовала всё
это время не установка плагина, а часть раскладки проекта, и узнавалась она
следом на диске. Перечень внешнего сократился до двух — opsx и av-dev-git.
Ветки «плагина нет» переписаны на «этой части в проекте нет»; там, где ветка
существовала только ради неразрешимого пути в чужое дерево, она снята вовсе.

Внутриплагинные копии языка и словаря сопровождения сняты: два справочника по
213 строк и один по 34 заменены ссылкой на общий дом. Копии остались там, где
текст обязан лежать внутри промпта, — в уставах вычитки. Заодно починены пути
$CLAUDE_PLUGIN_ROOT и относительные ссылки, разъехавшиеся с новыми именами
каталогов.
2026-08-13 10:18:04 +03:00
av de12a4d8a3 слияние: три плагина стали одним av-dev, скиллы получили префиксы
Каталоги, агенты и общие дома переехали в av-dev/; скиллы названы по прежнему
плагину — doc-*, task-*, code-*, с двумя смысловыми именами вместо тавтологии:
doc-sync вместо docs, task-track вместо tasks. Манифесты сведены к двум
плагинам. Пространства имён вызовов и пути внутри дерева переписаны машинно;
проза, которая называет прежние плагины отдельными, идёт следующим шагом.
2026-08-13 10:10:51 +03:00
av 142659bfd1 язык: чекпоинт и синк вошли в словарь, опиниативный снят
Три слова жили в текстах, не входя в закрытый словарь правила 6, — то есть
выглядели словарём, не будучи им. Решение по каждому своё.

Чекпоинт и синк внесены с определением: первый называет плановый стоп, на
котором ждут ответа, второй — сверку каждого документа канона с работой, с
обязательным отрицанием по нетронутым. Русские замены обоих называют либо исход,
либо обряд, но не саму работу.

Опиниативный заменён на «проход с мнением» в 13 местах и добавлен к снятому
рядом с конфляцией и гайдом. Копии правил пересобраны resync.py; версию канона
это не двигает — в репозиторий проекта отсюда ничего не уезжает.
2026-08-13 09:48:00 +03:00
av 96dafc9011 вычитка обслуживания: план называет глубину, уставы знают прогон без метки
Проходы берут вход, потолки и состав половин из метки, а на прогоне обслуживания
метки нет — оба взяли бы их наугад и молча по-разному. План сценария теперь
называет глубину прямо: basics — сверка с потолком 2, code — вход small, потолки
3 и 2 и включённая третья половина.

Третья половина code включена не для полноты: без неё security и architecture не
смотрит вообще никто. Границы покрытия переписаны честно — requirements не
смотрел никто, две другие темы сверены только против записанных инвариантов и
только если проход по коду шёл.

Уставы review-code и review-basics знают прогон без метки, их описания тоже;
триаж знает, что план сценария встаёт на место плана разметки. В скилле задач
уточнено: тип не выбирает метку, но предлагает сценарий, а меняется он командой
edit --type, а не исполнителем по ходу. Плюс язык: заголовок, два оборота и
right-size на русском.
2026-08-13 09:43:18 +03:00
av 95fed623e7 обслуживание: найденная дельта останавливает работу предложением, а не отказом
Стоп по найденной дельта-спеке говорил только «поведение меняется, дальше идёт
решение». Классификация при этом падала на человека в момент, когда весь
материал для неё у исполнителя, а задача выглядела сломанной, хотя она просто
оказалась шире своего типа.

Порядок теперь из трёх шагов: назвать тип, которым задача оказалась (fix —
расходится с заявленным, feature — снаружи появляется то, чего не было),
объяснить простым языком, что нашлось, и дать два решения — переформулировать
запись и решать процессом того типа следующим прогоном либо прекратить работу.
Третьего решения, «доделать как обслуживание», нет.

Тип исполнитель предлагает, меняет его av-dev-tasks:tasks и только после ответа:
переклеенный на ходу тип назначает себе другой процесс и другую глубину проверки.
Сделанное при любом решении остаётся в рабочем дереве незакоммиченным.
2026-08-13 09:30:52 +03:00
av 42849c13eb resolve: третий сценарий — обслуживание, у цикла SDD там нет входа
Задача, не меняющая поведения (тулчейн, зависимости, сборка, гит-хуки, перенос,
чистка), шла полным циклом решения. Все его шаги стоят на дельта-спеках, а у
chore их нет по построению: цикл не урезан ради дешевизны, он остаётся без входа.

Признак — связка: тип записи предлагает, отсутствие дельт подтверждает, а
расхождение признаков это стоп. Размер признаком не стал намеренно: «мелкое —
коротким путём» и есть самая дешёвая лазейка. Планового стопа у сценария нет
вовсе — объяснять человеку нечего, выбора там не делают; правило необратимого
поэтому действует жёстче, чем в двух других.

Ревью идёт фиксированным планом без метки и без разметчика — autotests и
operations, плюс conventions с техническим разбором, когда дифф трогает код.
Конвейер получил раздел «Прогон без change»: он написан вокруг change, и без
этой строки вызов упирался бы в предпосылку OpenSpec. Главный шаг сценария —
синк документации, а триггеры ADR работают стоп-признаком: своего источника у
обслуживания нет, и решение с ценой уходит в разведку.
2026-08-13 09:22:50 +03:00
av f22e7ed829 вычитку зовёт тот, кто правил, а не тот, кто синкал
Вычитка документов была привязана к синку, а разведка синком себя не считает —
и не доставалась ей вовсе. Условие вызова теперь правка: правил документы —
зови doc-wording, трогал записи — task-form и task-wording.

У разведки вычитка стала шагом 6, между записью и гейтом: пачка собирается из
шагов 4 и 5, раньше она не полна, после коммита правила бы уже историю. Гейт
с коммитом стал седьмым шагом, закрытие — восьмым.

Запрет остался, но только на судей канона: doc-consistency и doc-code-drift
идут на весь канон разом и зовутся через healthcheck.
2026-08-11 13:37:33 +03:00
av b3479776a4 канон: «промоут» уступает место русскому глаголу 2026-08-11 12:21:31 +03:00
av b8120d3271 resolve: два сценария вместо одной цепочки — разведка и решение
Разведка была прологом к коду: три шага, чекпоинт вариантов — и вливание в
общую ветку. Своего исхода у неё не было, поэтому и писать в документы проекта
ей было незачем: ответ оседал в design.md будущего change.

- у разведки появился исход: ответ уезжает в документы канона, задачи заводятся
  и уточняются, написанное коммитится, запись закрывается. Кода сценарий не
  пишет вовсе, OpenSpec ему не нужен
- точка входа осталась одна, и сценарий выбирает скилл, прочитав постановку:
  «есть ли очевидный способ решения» видно после чтения записи, и требовать
  этого суждения от вызывающего значит требовать его раньше, чем оно возможно
- оба сценария лежат справочниками и одинаково — solve.md и research.md, — а в
  SKILL.md остались вход, развилка и правила, не зависящие от сценария.
  Асимметрия читалась бы как старшинство: сценарий в теле скилла выглядит
  основным, а в справочнике — оговоркой
- переход между сценариями — событие с названным исходом: решение, упёршееся в
  незнание способа, останавливается; разведка, выбравшая способ, доводится до
  конца, а код идёт следующим прогоном, который запускает человек
- канон 14: у ADR два законных источника. У решения, принятого разведкой,
  design.md нет по построению, и такое решение либо не попадало в adr/ вовсе,
  либо попадало сочинённым заново

Правки по своему же ревью, до коммита:

- версия 14 была неполной — разрез проверки, вход и устав doc-consistency,
  скелеты adr/README.md и template.md по-прежнему требовали ссылку на
  design.md. Агент краснел бы на законной записи; скелеты уезжают в проекты,
  поэтому запись журнала называет их поимённо
- canon.md объявлял себя двенадцатым, пережив версию 13. Литерал был третьим
  домом числа при двух исправных — убран, а не поправлен
- сценарий разведки был недостижим там, где обещал работать: ready требует у
  research оба раздела, включая «Куда ляжет ответ», а сценарий брался назначить
  адрес сам. Адрес назначает автор записи; сценарий — только когда записи нет
- разведка коммитила без гейта, хотя правит документы канона и индексы задач
- при переносе выпало предупреждение про закрытие разведки без записанного
  ответа — возвращено
2026-08-11 11:36:37 +03:00
av 863769406f канон 13: файл версии зовётся по владельцу, у задач появилась своя версия формата
Имя `.pm.json` пережило плагин `av-dev-pm` на два месяца и указывало в пустоту.
Правило, которое из этого вынуто: имя служебного файла — имя плагина, который
его завёл, и по нему же владельца узнают.

- `docs/.pm.json` → `docs/.docs.json`, запись 13 журнала. Прежнее имя docs.py
  не читает намеренно: по этому числу upgrade решает, какие записи применять,
  и два дома разъехались бы молча ровно там, где это дороже всего. Вместо
  совместимости — узнавание: check видит старый файл и печатает готовую git mv
- у каталога задач появилась своя версия формата — ключ `tasks` в
  `.tasks.json`, свой журнал версий и своё повышение. До сих пор её не было
  вовсе, хотя docs.py в комментарии уверенно на неё ссылался: описание
  опережало механику ровно так, как сказано в решении 195
- число своё, а не копия канонического: плагин ставится в одиночку, и у
  проекта без docs/ версии канона нет — сверять было бы не с чем
- конфиг задач стал обязательным (init и adopt apply пишут его всегда), check
  сверяет число, `check --fix` его не приписывает: приписанное объявляло бы
  каталог приведённым к формату, шагов которого никто не делал
- переезды 11 и 12 в новый журнал задним числом не переписаны — версия 1
  велит догнать формат по журналу канона, называя признаки отставания
  поимённо (каталог в docs/tasks/, живой SPRINT.md)
- запись 60 в DECISIONS со следствиями 200–203; отдельно разведено с решением
  F, где `.docs.json` отвергался как указатель путей: отвергнут был указатель,
  а не имя
2026-08-11 10:35:39 +03:00
avandClaude Opus 5 12b77c3393 итог аудита: запись 59, карта домов про конвенции, план
- дом «что механизировано» называл conventions/README.md, хотя документ канона
  живёт файлом или каталогом; копия пересобрана resync
- запись 59: находки одного рода — правил механику, не правил описывающее её
  вовне. Пять выводов, включая «скелет не описывает, а порождает»
- в план добавлена пачка мелочи, оставленной сознательно, и сказано, что
  бумажная часть закрыта

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 18:48:21 +03:00
avandClaude Opus 5 12882911a9 словарь, манифесты, README: одно слово — одна вещь, одно описание — один дом
- «готовность» значила и «запись можно брать», и «что считается сделанным»;
  второй смысл стал «определением сделанного» — своё же правило про занятое
  слово запрещало это прямо
- «пайплайн» жил в 24 местах вне журналов при том, что DECISIONS фиксирует
  его уход «целиком»; рабочее имя — конвейер
- «чекпоинт» в review значил стадию и проход, в resolve — остановку человеку;
  слово оставлено за остановкой
- у описания плагина было два дома, и три из четырёх уже разошлись. Сведены,
  и класс закрыт машиной: frontmatter.py сверяет plugin.json с marketplace,
  гейт разбужен на *.json
- README врал про односторонние зависимости и терял healthcheck на диаграмме
- перечень агентов в REMAINING отстал на два поколения

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 18:45:39 +03:00
avandClaude Opus 5 63ba36d71d границы плагинов: путь в чужое дерево, безымянные стыки, звонящий у вычитки
Правило границы моё, копий восемь — и нарушал его я же.

- путь в дерево чужого плагина снят из пяти мест; маркер копии, уезжающий
  в проект скелетом, оставлен, но сказано, что сама пара маркеров не едет
- короткое имя чужого скилла в четырёх местах стало полным
- стык «урожай ревью → задачи» не был назван ни с одной стороны, хотя
  механика написана с обеих; теперь назван, с веткой «плагина нет»
- resolve звал av-dev-git:commit без строки доклада и пересказывал формат
  коммита, нарушая собственное «ссылайся, не пересказывай»
- doc-wording обещал момент вызова, которого не исполнял никто. Правило:
  звонящий — тот, кто только что писал текст. Вызов появился шагом в docs,
  init, adopt и upgrade; healthcheck по-прежнему его не зовёт
- openspec.py искал SHALL по всему файлу, а образец даёт его в context —
  проверка молчала ровно в том случае, ради которого написана
- фаза 2 review-rubric была недостижима; проход стал судить задуманное,
  а не код, и это сходится с тем, что о нём говорит конвейер
- rules.tasks в образце конфига, ветка «записи задачи нет» у review-scope,
  возвраты на чекпоинт в схеме resolve, старшинство правила дельта-спек

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 18:22:35 +03:00
avandClaude Opus 5 15dba79993 приоритет стал исполнимым: move без --section и тексты, догнавшие правило 4
Механизм расстановки приоритета не запускался ни разу: --section у move был
обязательным, а все три места, где груминг его предписывает, дают команду без
него — usage error. Чиню скриптом, а не текстами: перестановка внутри секции —
самая частая операция груминга, и требовать повторить текущую секцию значит
приглашать указать не ту.

- move: --section необязателен, без него берётся секция из индекса; сообщение
  различает перестановку и перенос
- докстринги, отрицавшие правило 4 («в беклоге порядок значения не имеет»),
  приведены к действительности
- reopen ставил возвращённую строку после сырья и давал ошибку check на ровном
  месте
- edit портил написание секции в мете; корень шире — брался нижний регистр из
  разбора, а не написание заголовка. То же в close и reopen
- дыра гейта: между заведением и ready запись не судил никто. Своя строка
  здоровья check, отдельная от «готово к взятию» — она про другое
- шесть файлов и два устава обещали, что схему типа проверяет check
- раздел from-review о серьёзности стоял на «приоритетов нет»
- остатки спринта и сессии в семи местах, включая description агента формы
- индексов два, а не три; два определения порога готовности после adopt

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 18:03:28 +03:00
avandClaude Opus 5 c1890d9e71 скелеты канона догнали канон 12: спринт, версия, чужая проверка
Аудит четырьмя сабагентами показал систематическую дыру: механика правилась,
а описывающее её вовне — нет. Дороже всего скелеты: они уезжают в проект.

- слоты спринта в скелете CLAUDE.md стали слотами груминга, имена взяты
  у groom, а не выдуманы заново — журнал версии 12 их уже назвал
- "canon": 11 в двух образцах стал плейсхолдером: литерал протухал третий
  раз подряд, а незамещённый плейсхолдер ломает разбор громко
- обещание, что docs.py проверяет форму openspec/config.yaml, снято из трёх
  мест; владелец назван полным именем, с оговоркой об отсутствии плагина
- adopt ставил в гейт проекта один шаг из трёх; соседские ставятся по следу
  присутствия, следа нет — строка доклада
- битая ссылка на tasks/ROADMAP.md из скелета паспорта

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 17:40:14 +03:00
av 4354cc4146 healthcheck: у судей документов появился свой скилл и свой момент
doc-consistency и doc-code-drift звались шагом сессии между спринтами.
Сессия стала грумингом, груминг судит задачи, а не документы, и звать
чужих агентов не вправе — они в av-dev-docs. На живом проекте их не
звал бы никто, кроме разовых adopt и upgrade.

Момент назван у владельца. Разрез с canon check проверяемый: машина
сверяет форму, healthcheck — утверждения.

Почему скилл, а не просто описания агентов: двоим нужна оркестровка —
позвать обоих на весь канон разом, передать doc-code-drift раздел
запретов, разобрать урожай порциями, назвать границы покрытия и кого
именно позвал. Этого агент о себе не знает.

doc-wording внутрь не взят: ему оркестровка не нужна, и ритм другой —
он нужен там, где текст только что писали.

Заодно из shared/plugin-boundary.md и README убран счётчик скиллов: он
протух дважды за день.
2026-08-09 16:58:51 +03:00
av df5af47dc3 хвосты после снятия спринтов: четыре штуки
resolve судил готовность записи глазами — текст писался до того, как
появилась команда ready. Теперь зовёт её через av-dev-tasks:tasks, а
отказ читает исходом «не доведена». Это ровно тот случай, где машина
дешевле и точнее, а цена ошибки отложенная.

canon.md всё ещё говорил, что невзятой запись делает sprint take.

REMAINING перечислял три опоры против совпадения приёмщика с
исполнителем, включая SPRINT.md под git. Опор осталось две, и третья не
заменилась, а исчезла: у приёмки больше нет момента. Записано как есть.

frontmatter.py: устаревший путь review-pipeline/SKILL.md в комментарии.

В план добавлена дыра, которую открыло снятие сессии: двух агентов
канона на живом проекте больше не зовёт никто, кроме adopt и upgrade.
2026-08-09 16:53:20 +03:00
av 4a56753f0b session стал groom: два вопроса вместо ритуала спринта
Предмет сузился до двух: что сейчас самое важное и что перестало быть
важным. Ответ записывается порядком строк в беклоге.

Из четырёх шагов сессии выжили два (вопросы, переоценка порциями), один
заменился расстановкой очереди вместо набора спринта, два выпали.
Приёмка — грумингу не по предмету, ритуала у неё больше нет, остаётся
reopen; цена названа в тексте. Разбор процесса потерял якорь, и вместе
с ним ушёл прямой вызов агентов doc-consistency и doc-code-drift — это
починка, а не потеря: агенты принадлежат av-dev-docs, и груминг звал их
мимо правила обращения к соседу.

sprint.md удалён, cadence.md стал portions.md.

Канон 12: запись в журнал велит проектам снести SPRINT.md и расставить
порядок, с точным порядком шагов — сперва удалить файл, потом check
--fix, иначе он увидит третий индекс и станет ругаться, а не чинить.

Побочно: canon.md объявлял себя версией 7 при текущей 11. Пять версий
дом канона врал о себе — машина сверяет константу скрипта, а прозу в
заголовке не читает никто.
2026-08-09 16:49:07 +03:00
av 3653c5cff5 скилл tasks: приоритет стал порядком строк в беклоге
Правило 4 переписано целиком. Было «порядка нет, есть цель», и
обосновано это было тем, что на «что делать дальше» отвечает набор
спринта. Набора нет — вопрос остался, отвечать нечем.

Приоритет — свойство очереди, а не задачи, поэтому его дом индекс: то
же исключение из правила 2, что и «в каком индексе лежит запись».
Положи его в файл числом — два соседних файла смогли бы утверждать одно
место, а строка индекса противоречить обоим. Цель и приоритет —
независимые оси: очередь может идти поперёк целей.

Расстановка — это move --after и move --first, и только они: руками
поправленная строка не оставляет причины.

Место сырья в конце секции из очереди изъято: оно производно от типа и
заполненности, его назначает машина, приоритетом оно не становится.

Схема состояний потеряла SPRINT.md и четыре перехода; шесть уставов
типов, task-format, split, from-review и adopt переведены со «взятия в
спринт» на ready.
2026-08-09 16:36:26 +03:00
av a73eedb893 tasks.py: спринт снят, гейт готовности переехал в команду ready
Спринт был вплетён в 215 мест: конфиг, разбор индексов, состояния
задачи, add/edit/move/close/reopen, блок здоровья и CLI. Снято всё:
SPRINT.md, четыре команды sprint, автотег урожая sprint:<слаг>,
проверки набора в check.

Гейт готовности стоял на sprint take — единственном месте, где запись
судили целиком. Момент нужен и без спринта, иначе задача уезжает в
работу без критериев приёмки. Теперь это команда ready <слаг>: тип,
цель у feature, пустой раздел вопросов, схема типа. Отказ там рабочая
ситуация, а не ошибка употребления, — код 1, не 2.

Приоритет стал порядком строк в беклоге, и два места это уже знают:
reopen и check --fix ставят восстановленную строку в конец секции и
говорят, что позицию назначает человек. Молчаливое восстановление
выдавало бы машинную позицию за его решение.

Переезд проекта: снести SPRINT.md, прогнать check --fix — задачи из
набора вернутся в беклог. Проверено на фикстуре.
2026-08-09 16:30:27 +03:00
av 1bce854535 сверка после реорганизации: два неучтённых следа
REMAINING держал два утверждения, которые переименование сделало
неверными.

Перечень непрогнанного не знал про resolve — а это самый новый скилл
и единственный с чекпоинтами.

Риск «скиллы jellybit названы ровно как в плагине» снят: там
task-pipeline, review-pipeline, task-batch, а плагин теперь даёт
resolve, review, openspec — пересечений нет. Записано, что риск снят
переименованием, а не устранён по существу.

Заодно список проектных копий в resolve дополнен именами прошлого
поколения: сносить надо и их.
2026-08-09 15:52:13 +03:00
av ee53ef8af8 resync.py поднят в scripts/: пересборка копий из домов
copies.py находит расхождение, а чинить его руками — та же работа, из-за
которой копии и расходятся: правка дома касается стольких файлов, сколько
у него копий, и последний забывают. Проверка была, починки не было.

Разметку разбирает не свой код: copies.py импортируется целиком, иначе
второй разборщик той же разметки разошёлся бы с первым молча.

В гейт не ставится намеренно. Автоматическая пересборка протащила бы
правку дома во все копии мимо глаз автора, а копия, уезжающая в
репозиторий проекта, тянет ещё и запись в журнал версий канона — этого
машина не напишет.

Проверено на обеих формах копии: обычной и лежащей внутри объемлющей
ограды. Ограда и пустые строки по краям принадлежат месту, а не дому, и
пересборка возвращает их такими, какими были — round-trip побайтно
чистый на всех 24 копиях.
2026-08-09 15:49:45 +03:00
av 53cf6baedf av-dev-pipeline стал av-dev-code, review-pipeline — review
Имя описывало устройство, а не предмет: «пайплайн» говорит, что внутри
конвейер, — а плагин занят кодом по задачам, и с появлением чекпоинтов
он уже не конвейер в чистом виде. Набор имён стал параллельным:
docs / tasks / code / git, каждое называет материал.

Заодно review-pipeline стал review — слово ушло из плагина целиком, а
не наполовину; скиллы выровнялись: resolve / review / openspec.

Журнал версий канона переписан вместе со всеми, DECISIONS.md — нет.
Разрез по типу высказывания, а не файла: наблюдение и причина
неприкосновенны, предписание и адрес обязаны оставаться исполнимыми.
Запись версии 10 велит «проверить, что плагин av-dev-pipeline
установлен» — проект, дошедший до неё, выполнил бы невыполнимое.
2026-08-09 15:43:03 +03:00
av c5e6883461 task-pipeline переписан в resolve: два плановых стопа
Автоматическое решение задач агентом работает плохо, и хуже того — в
процессе перестаёт ориентироваться автор. В цикл возвращается человек,
но не согласованием на каждом шаге: доктрина «делать, а не спрашивать»
не отменена, а ограничена местом. Развилка до ближайшего чекпоинта
копится в него, после последнего — уходит вопросом в запись.

Чекпоинтов два. «Варианты» — у исследовательской задачи, до первого
требования: 2-4 способа с ценой каждого и рекомендацией, выбор оседает
по адресу, который назвала сама запись. «Объяснение» — у всякой, после
ревью дизайна: человек читает просеянное машиной.

Объяснение не завело своего артефакта — оно собирается из proposal.md и
design.md, а требование к их форме уехало в openspec/config.yaml
(rules.proposal, rules.design), то есть применяется в момент написания.
Отдельный раздел был бы третьим домом одного объяснения.

Закрыт открытый вопрос: находка ревью, меняющая дельта-спеки, отменяет
одобрение — разметка пересчитывается, чекпоинт повторяется.
2026-08-09 15:34:13 +03:00
av c3828b3713 task-batch удалён вместе с хвостами
Пайплайн нескольких задач снят с повестки: работаем по одной. Каталог
скилла удалён, и с ним всё, что держалось только на нём.

Хвосты были не только ссылками. У review-specs исчез третий режим
(стык после слияния) — вместе с исключением «живого change нет, берём
актуальные спеки источником»: теперь отсутствие дельта-спеки это отказ.
У review-triage исчезло единственное исключение из правила «плана нет —
не запускаюсь». В каноне и у doc-code-drift имя основной ветки
обосновывалось тем, что «в неё вливает батч», — довод заменён на
верный.
2026-08-09 15:25:54 +03:00
av 872732989a адреса чужих документов сверяются с перечнем владельца
Адрес принадлежит одному плагину, а называют его все: docs/* стоит в
сорока местах конвейера, tasks/ROADMAP.md — в четырёх местах канона.
Переименование в каноне до них не доходит, и заметить это нечем:
протухший адрес попадает в механизм честной деградации ревью и выходит
правдоподобной строкой «документа в проекте нет», а не поломкой.

Перечень берётся из константы владельца — той, по которой он и так
проверяет раскладку. Судится упразднённое, а не незнакомое: список тем
канона открытый, и «нет такого имени» опровергнуть нечем; зато карта
переездов RETIRED и есть перечень запрещённого. Рядом одна догадка —
почти совпавшее имя как опечатка, порог замерен (законные до 0.64,
опечатки от 0.91).

Первый прогон: одна настоящая находка — REMAINING иллюстрировал
смысловой дубль адресом docs/specs/, упразднённым в версии 1 канона.

В гейте без glob: перечень лежит в .py, упоминания в .md, и коммит с
переименованием трогает только первую сторону.
2026-08-09 15:12:46 +03:00
av c6be879831 правило обращения к соседнему плагину получило дом
Стояло в пяти местах в пяти редакциях: два разных довода, ни в одном
месте оба, и три места из пяти молчали о том, что делать при
неразрешившемся вызове. Плюс невысказанный инвариант: $CLAUDE_PLUGIN_ROOT
ведёт только в свой плагин.

Дом — shared/plugin-boundary.md, блок «граница-плагинов», семь помеченных
копий. В дом вошло правило, последствия остались на местах вызова:
«нет плагина задач — учёт остаётся владельцу» знает только конвейер.

Оглавление адресов в CLAUDE.md проекта отклонено — второй дом раскладки,
и протухший адрес в нём выходит правдоподобной строкой честной
деградации. Сверка адресов уходит машине, записана в TODO разделом 5.
2026-08-09 15:02:46 +03:00
avandClaude Opus 5 c91492e3f0 TODO переписан с чистого листа; canon и docs остаются раздельно
План описывал мир до раскола: av-dev-pm в живых, каталог задач в docs/, шаги
повышения на каноны 3, 4 и 5 — при том что канон уже 11. Двести с лишним строк, из
них живых полтора десятка, и найти их можно было только прочитав всё.

Умер он двумя способами сразу, и оба записаны в новом заголовке, чтобы не
повторились. Первый: сделанное помечалось галочкой и оставалось в файле — список
из двух сотен [x] перестают читать целиком, и живые пункты в нём теряются. Теперь
сделанное удаляется, след остаётся в коммитах и DECISIONS. Второй: план построчно
повторял записи журнала версий канона, то есть был вторым домом для шагов
повышения, и половина повторов протухла молча. Теперь на журнал стоит ссылка.

Новый план — пять разделов: вернуть живые проекты в рабочее состояние, учёт работ
без спринтов, калибровка, пайплайн одной задачи в три этапа, обкатка. Поимённой
раскладки файлов healthlog в нём нет намеренно: её знает canon adopt, и второй
перечень разошёлся бы со скиллом. Зато названо то, чего скилл не сделает и что
легко потерять — гейт проекта теперь три шага вместо одного, потому что docs.py
перестал тянуть за собой и задачи, и форму config.yaml.

REMAINING ссылался на разделы TODO по номерам — переведён на имена; заодно строка
про непрогнанное на живом проекте дополнена скиллом openspec и оговоркой, что
раскол проверен только на фикстурах и на установке каждого плагина в одиночку.

Решение 53: canon и docs остаются двумя скиллами. Довод не про объём, а про
description — это триггер, по которому загрузчик решает, звать ли скилл, и
моменты вызова у этих двух разные. Слитое описание покрывает оба хуже, чем два
покрывают каждое своё.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 14:40:16 +03:00
avandClaude Opus 5 e408c51ac1 README: скилл openspec в диаграмме вызовов
Скилл появился коммитом раньше, а в карту «кто кого зовёт» не попал: init и canon
зовут его наравне с tasks, и без этих двух стрелок диаграмма утверждает, что
OpenSpec никто не заводит.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 14:35:59 +03:00
avandClaude Opus 5 f40e0cd7bb каталог задач уехал из docs/ в корень репозитория
Версия 8 отпустила задачи из канона — перестала требовать каталог и перестала в
него смотреть, — но место он занимал всё то же, docs/tasks/. Полдела: каталог,
принадлежащий одному плагину, лежал внутри дерева, которым владеет другой.
Проекту, поставившему учёт работ без канона документов, приходилось заводить
docs/ ради одной вложенной папки.

Дом задач теперь tasks/ в корне. tasks.py ищет его там первым, docs/tasks и
doc/tasks остались в списке поиска для непереехавших проектов, init заводит
только в корне, --target у adopt тоже. Настройки лежат рядом, tasks/.tasks.json —
после версии 8 они уже были в своём файле, теперь и файл вне чужого дерева.

docs.py продолжает терпеть docs/tasks/ в списке нечитаемого: непереехавший проект
не должен получать выдуманную ошибку «файл вне канона» вдобавок к записи журнала,
которая и так велит ему переехать. Адреса упразднённых слотов (plan.md, backlog/)
теперь ведут в tasks/ и называют плагин.

Тридцать три живых упоминания пути разведены по смыслу, а не заменой строки: в
раскладке канона tasks/ вышел из-под docs/ и стоит на своём уровне; у
doc-consistency каталог перестал быть исключением внутри docs/** и стал чужой
территорией, названной в обеих формах; у review-scope и review-pipeline
процессный список поехал вместе с путём. В журнале версий тринадцать упоминаний
оставлены как есть — они описывают прошлое состояние.

Канон повышен до версии 11. В записи названа тихая часть переезда: файл
tasks/items/x.md стал на уровень ближе к корню, и ../../passport.md в теле записи
теперь ../docs/passport.md. Битую относительную ссылку внутри записи tasks.py
check не ловит вовсе — её видит только docs.py и только у документов канона, так
что этот шаг делается руками и тем же коммитом, что git mv.

Фикстура переехала, поиск вверх по дереву находит новый путь, гейт зелёный.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 14:33:33 +03:00
avandClaude Opus 5 fdadfb65ac валидатор config.yaml переехал в конвейер: у пайплайна свой скрипт
Решение 51 отдало OpenSpec конвейеру и честно оставило хвост: проверка формы и
сторож версии остались в docs.py, потому что своего скрипта у пайплайна не было
ни одного. Хвост не косметический — это ровно то состояние, против которого
написан весь канон: у файла два владельца, один заводит, другой проверяет, и
разойтись они могут молча.

252 строки переехали в av-dev-pipeline/skills/openspec/scripts/openspec.py: пять
проверок формы, сторож версии, сверка слепка с живым инструментом. Команды две —
check --dir <корень> и form; коды выхода общие со всеми скриптами av-dev. Из
docs.py удалены константы OPENSPEC_*, check_openspec, openspec_cli,
check_openspec_fresh, rules_keys и подкоманда openspec-form; про config.yaml он
больше не говорит ничего, кроме строки границы механизируемого — что форму
смотрит чужой скрипт. openspec/specs/ он по-прежнему знает: это дом темы
requirements и часть карты тем.

Переезд оплатился сразу, и не тем, чего ждали. Прежняя проверка требовала, чтобы
context называл docs/passport.md и CLAUDE.md, безусловно — то есть на проекте без
канона документов требовала ссылку на несуществующий файл. Пока код жил в скрипте
канона, допущение «канон есть» было незаметным: скрипт канона запускают там, где
канон есть. В скрипте конвейера то же допущение стало видно на первом прогоне.
Теперь адрес требуется только к существующему документу, отсутствие идёт строкой
«не проверялось» с названной ценой — без канона конвейер работает вслепую.

Заодно починен хвост от раскола плагинов: pyrefly project-includes в pyproject
всё ещё указывали на av-dev-pm. Линтер на явных файлах работал, а на обходе
проекта не проверял ничего.

Проверено пятью случаями: нет openspec (1), годный конфиг (0), опечатка в имени
артефакта под rules (1), проект без канона (0, с двумя строками «не
проверялось»), неизвестная команда (2).

Канон повышен до версии 10. Главное в записи — тихая потеря: форму раньше
проверял docs.py check заодно, теперь нужен отдельный шаг openspec.py check в
гейте, иначе незаменённый пример в config.yaml перестанет ловиться. Решение — 52.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 14:28:12 +03:00
avandClaude Opus 5 9453a218d1 журнал решений: запись 51 про раскол плагинов
Три коммита раскола несут причины в теле, но дом у решения один. Записано то,
чего в отдельных коммитах не видно: сцепка внутри одного владельца не выглядит
сцепкой, пока владелец один, и обнаруживается не рассуждением, а попыткой
поставить половину отдельно. Плюс правило разреза владения — по тому, кто
инструментом пользуется, а не по тому, кто о нём подробнее написал.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 14:18:06 +03:00
avandClaude Opus 5 f0dd8f70c1 openspec уехал в конвейер: заводит его пайплайн, канон только высказывается
Версия 7 объявила openspec/ слотом канона: init его заводил, adopt тоже, образец
config.yaml лежал в скелетах, отсутствие каталога docs.py считал отказом. Разрез
был проведён не там. По OpenSpec работает конвейер — без каталога не запускаются
ни opsx:propose, ни ревью дизайна, ни сверка требований, — а канон документов о
нём только высказывался. Проект, которому конвейер не нужен, получал отказ за
отсутствие того, чем не пользуется.

Появился скилл av-dev-pipeline:openspec: заводит каталог, заменяет
закомментированный пример в config.yaml настройкой, объясняет разрез между
ссылкой и пересказом — утверждение, опровергаемое открытием другого файла, это
пересказ; строка, говорящая какой файл открыть, это ссылка. Образец переехал туда
же, в references/config-skeleton.md, а в скелетах канона остался указатель.

init и canon adopt OpenSpec больше не заводят, а зовут скилл конвейера через
пространство имён. Вызов не разрешился — плагина конвейера нет, и это строка
доклада, а не поломка: docs.py о каталоге тогда тоже молчит. Отсутствие openspec/
стало неприменимостью вместо отказа, остальные четыре проверки формы идут только
при живом каталоге. На фикстуре без openspec дрейф упал с 10 пунктов до 9.

Что осталось на месте и названо честно: проверка формы config.yaml и сторож
версии (docs.py openspec-form) пока живут в скрипте канона. Перенести их значит
завести в конвейере свой скрипт, а этого у него нет ни одного. У файла сейчас два
плагина — один заводит, другой проверяет, — и это временное состояние, а не
задуманное; в журнале версий оно записано так же.

Канон повышен до версии 9. Запись не двигает ни одного файла проекта: меняется
только то, кто их заводит. Но в ней названа потеря, которую легко не заметить —
проект по OpenSpec без установленного пайплайна теперь не услышит от docs.py
ничего про свою настройку, и молчание это законное.

Гейт зелёный, скиллов стало десять.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 14:17:19 +03:00
avandClaude Opus 5 1f31ac6afd канон отпустил каталог задач: docs.py не зовёт tasks.py, конфиг разъехался
Пока владелец был один, docs/tasks/ числился слотом канона: docs.py требовал
каталог, звал внутрь чужой скрипт подпроцессом и выдавал его дрейф за свой, а
настройки задач жили ключом tasks в docs/.pm.json. Для проекта, поставившего
только документы, всё это отказ на ровном месте — задач он не ведёт, и требовать
их не за что.

Раскол вскрыл это немедленно и молча: check_tasks искал tasks.py по пути
parents[2]/tasks/scripts, то есть внутри своего плагина, и после переезда
скатывался в ветку «скрипт не найден» на каждом прогоне. Проверка выглядела
живой и не проверяла ничего.

Теперь docs.py про задачи не говорит ни слова: check_tasks снят целиком, каталог
остаётся в NOT_DOCS, его отсутствие дрейфом не считается. Канон резервирует место
в docs/ и внутрь не смотрит.

Дом настроек каталога задач вернулся в свой файл — <каталог>/.tasks.json. Прежний
ключ tasks в docs/.pm.json читается, только когда своего файла нет, и скрипт
говорит, куда его перенести; есть оба — побеждает свой, и об этом тоже говорится
вслух. Порядок именно такой, потому что docs/ принадлежит другому плагину: дом
настроек в чужом дереве это дом, которого у половины проектов нет.

Заодно закрыта дыра, которую сам же и открыл первый вариант правки: битый
docs/.pm.json ронял бы задачи даже при живом своём конфиге. Чужой файл здесь
только повод для замечания, и его поломка не наша. Проверено на четырёх случаях —
только чужой конфиг, оба, свой плюс битый чужой (код 0), только битый чужой
(код 3, окружение).

Канон повышен до версии 8 с записью, выполнимой upgrade. В ней названо и то, что
легко потерять: раньше согласованность задач тянул за собой docs.py check, и
проект, у которого в гейте стоял только он, обязан добавить второй шаг — иначе
дрейф индексов перестанет ловиться молча.

Гейт зелёный. Оба скрипта прогнаны: docs.py check на фикстуре про задачи не
упоминает, tasks.py check код 0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 14:12:08 +03:00
avandClaude Opus 5 00ddfb0dde av-dev-pm расколот на av-dev-docs и av-dev-tasks
Плагин владел двумя разными вещами сразу — документацией проекта и учётом работ,
— и это мешало обеим. Канон нельзя было поставить без задач, задачи без канона, а
язык проектных текстов лежал внутри скилла canon и потому принадлежал половине.
Теперь плагина два, каждый ставится сам по себе.

av-dev-docs: скиллы canon, docs, init; агенты doc-consistency, doc-code-drift,
doc-wording; скрипт docs.py. av-dev-tasks: скиллы tasks, session; агенты
task-form, task-wording; скрипт tasks.py.

Между собой они зовутся через пространство имён, а не по пути в чужое дерево.
Все относительные ссылки, пересекшие границу плагина, сняты: tasks больше не
указывает в canon, canon не указывает в tasks. Вместо ссылки — имя скилла и
оговорка, что вызов может не разрешиться, и это исход, а не поломка.

То, что нужно обоим дословно, стало вторым общим домом. Словарь «Сопровождение и
эксплуатация» назван в трёх местах трёх плагинов — секция роадмапа, раздел
«Эксплуатация» в architecture.md, тема ревью operations — и ни один из трёх им не
владеет; он уехал в shared/operations.md, а canon.md и скилл задач везут копии.
Три перечня «чем держат проект» уже разъезжались на «метриках и логах» против
«мониторинга», так что ссылка тут не годится: плагин, поставленный в одиночку,
получил бы указатель в никуда. Тем же способом язык: у av-dev-tasks появилась
своя копия language.md.

Копий стало 18 при 8 домах.

Переименования разведены по смыслу, а не заменой строки: где речь о каноне —
av-dev-docs, где об учёте задач — av-dev-tasks. В пайплайне таких мест
одиннадцать, и оба адресата там встречаются вперемешку.

Журналы (DECISIONS, TODO, HISTORY) намеренно не тронуты: они описывают состояние
на момент записи. По той же причине оставлена наблюдённая строка в комментарии
docs.py — она цитирует конфиг живого проекта, а не называет плагин.

Не входит в этот заход и названо отдельно: слияние canon и docs в один скилл,
разделение docs/.pm.json на два конфига и переезд openspec в пайплайн.

Гейт зелёный: копии, фронтматтеры, диаграммы, json. Оба скрипта прогнаны после
переезда — docs.py version и tasks.py check на фикстуре.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 14:06:26 +03:00
avandClaude Opus 5 86e22d932c вычитка раздвоилась: doc-wording для документов, task-wording для записей
Решение ППП говорило: агент называется doc-wording, а не task-wording, потому что
правила языка относятся ко всем проектным текстам, а не к одним задачам.
Утверждение верно и сегодня — оно и есть причина, по которой правила уехали в
shared/. Но из общности правила не следует общность прохода: docs и tasks
расходятся самодостаточными плагинами, а самодостаточный плагин не может
зависеть от агента соседа. ППП отменено, и отменено не по своей оси.

Проходов теперь два, и разведены они по охвату — впервые в этом репозитории. И
task-form против вычитки, и doc-consistency против doc-code-drift разведены по
глубине; здесь глубина одна, а входы разные. doc-wording читает документы канона,
конвенции, ADR, записки разведки и CLAUDE.md; task-wording — items/ и строки
индексов, а документы проекта открывает только как словарь, чтобы отличить
неизвестный термин от известного.

Разрез по охвату дублирует устав, и потому весь общий текст стал домом. Оба
судят по одним и тем же девяти правилам; отличаются входом, соседями по границе,
машинной проверкой, о которой молчат (docs.py против tasks.py), и способом
подстановки — команда edit у задач, редактор у документов. Копий в каждом уставе
151 строка, своего непустого текста 61 и 75.

Домом стал и формат доклада — блок вычитка-доклад: форма находки, границы
покрытия, пустой доклад. Это контракт прохода, а не правило языка, но лежит он в
shared/language.md отдельным разделом: заводить под пятнадцать строк отдельный
файл дороже, чем назвать раздел честно. Признак дома здесь не тема, а число
потребителей больше одного при обязательной дословности — разойдись два прохода
формой доклада, зовущий скилл разбирал бы два формата вместо одного.

Ссылки разведены в семи местах: tasks/SKILL.md (таблица двух проходов),
task-form (описание и обе границы), doc-code-drift, canon.md (сравнение разрезов),
canon/SKILL.md, README дважды. doc-consistency и таблицы канона оставлены на
doc-wording — они про документы.

Гейт зелёный: копии 13 при 7 домах, фронтматтеров 24, диаграммы. Решение — 50.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 13:54:46 +03:00
avandClaude Opus 5 c669215fc8 язык уехал в shared: дом вне плагинов, устав вычитки — копия целиком
Дом языка лежал в av-dev-pm/skills/canon/references/language.md — внутри одного
скилла одного плагина. Пока плагин был один, это читалось как «дом рядом с
главным потребителем». Разделение на самодостаточные docs и tasks превращает то
же место в утверждение, что язык принадлежит канону: плагин задач, поставленный
без канона, потерял бы правила письма вместе с ним. Дом переехал в shared/ и не
принадлежит ни одному плагину, плагины везут дословные копии. Самодостаточность
держится копией, а не ссылкой: shared/ нужен этому репозиторию, а не
установленному плагину.

Устав вычитки стал копией целиком. doc-wording копировал из дома англицизмы,
словарь, жаргон и порог правки — четыре блока; девять правил он излагал своими
словами, и эти слова с домом никто не сверял. Там дрейф и копился молча: в доме
правило «одна мысль — одно предложение» требовало выносить придаточное, в уставе
— не резать причинную связь, и каждая версия выглядела полной. Из десяти правил
машина сверяла четыре, теперь сверяет все.

Условие переезда: текст правил написан безлично, а всё, обращённое к проходу
(«пиши так-то», «про это молчи»), вынесено из блока в раздел «Что из этих правил
докладывается особым образом». Правило принадлежит дому, способ доложить о нём —
уставу.

Блоков три, и делятся они по потребителю, а не по теме: язык-доктрина (зачем
стиль, полезное действие, параллельность, заголовок, что отброшено),
язык-правила (девять правил с тремя таблицами), порог-правки. Порог оставлен
отдельным потому, что его берёт task-form, который правил языка не проверяет
вовсе; вложенных блоков copies.py не знает, и внутри язык-правила забрать порог
было бы нечем.

Потребители собраны из дома скриптом, а не руками. Домов 6 вместо 7 — три
таблицы слились в язык-правила; копий 9 вместо 8.

Гейт зелёный: копии, фронтматтеры, диаграммы. Решение — 49.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 13:49:21 +03:00
avandClaude Opus 5 6ff12fedd5 форма config.yaml сверяется с живым openspec, а не с памятью
Проверка формы знала имя схемы и перечень артефактов константами — и это не наше
решение, а состояние чужого инструмента. OpenSpec переименует артефакт: правила
под прежним именем перестанут применяться, конфиг останется выглядеть
написанным, канон продолжит требовать прежнее. Молчат при этом все три стороны,
и заметить расхождение было некому.

Сторожем поставлено сравнение версий. check спрашивает openspec --version —
десятые доли секунды — и сравнивает major.minor с той, на которой форма
сверялась. Разошлось — замечание, не отказ, с именем команды, которая
перепроверяет. Патч-версия в сравнение не берётся намеренно: формы она не меняет,
а нагоняй на каждый багфикс приучает пролистывать весь блок.

Перепроверяет docs.py openspec-form: берёт openspec templates --json, то есть
перечень артефактов текущей схемы, и печатает, что разошлось с константами.
Дорогой вызов вынесен из check сознательно — он стоит втрое дороже опроса версии,
а ответ меняется только вместе с версией. Дешёвая проверка служит воротами
дорогой, и дорогая не ржавеет, потому что зовут её не по памяти. Чинится
расхождение в плагине, а не в проекте, и команда печатает три адреса правки
списком: константы скрипта, скелет, журнал версий канона.

Пятой проверкой формы стали ключи под rules: — это имена артефактов, и правило,
адресованное несуществующему, не применяется молча. rules.spec вместо
rules.specs даёт конфиг, выглядящий написанным и не работающий.

Первый вариант этой проверки искал ключи отступом по всему файлу и нашёл их
внутри литерального блока context: строки «Language: Russian» и
«av-dev-pm:review-pipeline» выглядят ключами. Оба живых проекта из-за этого
покраснели на правде. Теперь разбор идёт от строки rules: до следующего ключа
нулевой колонки; на тех же проектах чисто, а опечатка в имени артефакта
по-прежнему находится.

Решение — 48.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 12:07:46 +03:00
avandClaude Opus 5 a79266cfcb init заводит openspec сам; конфиг стал слотом канона
Каталог openspec/ был предпосылкой, о которой канон говорил, но за которой не
следил. openspec/specs/ объявлен домом темы requirements, config.yaml описан
абзацем — а заводилось всё руками, и не проверялось ничего. Новый проект выходил
из init с полным каноном документов и без каталога, без которого не работают ни
opsx:propose, ни ревью дизайна, ни сверка требований.

Теперь init делает openspec init --tools claude шагом 3, до первого документа, а
adopt заводит его тем же способом, если на переводимом проекте его нет. Команда
названа поимённо в трёх местах — скилле, каноне и отказе docs.py: отказ без
команды заставляет искать её в другом месте.

Файл из коробки оказался хуже отсутствующего, и потому проверяется машиной.
openspec init кладёт config.yaml, где context и rules — закомментированный пример
на английском. Такой файл читается как настроенный: он есть, он валиден, имя
правильное. Работает он как пустой, и узнаётся это по уже написанному
предложению — на другом языке, с capability по имени пакета, без единого SHALL.
docs.py проверяет четыре вещи, каждая про молчащий пробел: каталог есть; имя
именно config.yaml (config.yml OpenSpec не читает и об этом не сообщает); context
и rules.specs не остались примером, а правила называют SHALL; context называет
passport и CLAUDE.md. Последние два обязательны по порядку работы: предложение
пишется до того, как кто-либо откроет docs/, и без этих строк его пишут, не зная
ни границы домена, ни инвариантов.

Форма конфига записана скелетом и сформулирована разрезом: утверждение, которое
можно опровергнуть, открыв другой файл проекта, — пересказ; строка, которая
говорит, какой файл открыть, — ссылка. Машина этот разрез не проверяет, отличить
одно от другого она не умеет; он отдан doc-consistency отдельным абзацем правила
«один факт — один дом», и config.yaml добавлен ему во вход. Место второго дома
там самое частое: context читается при порождении каждого артефакта, туда удобно
дописать «чтобы агент знал», и так заводятся копии инвариантов, конвенций,
состава гейта и правил ревью.

Образец лёг в канон, а не в конвейер, как планировало решение C: форма документа
принадлежит владельцу канона документов, конвейер её читатель. Иначе
av-dev-pipeline завёл бы описание файла, который заводит и проверяет av-dev-pm.

Канон повышен до версии 7 с записью, выполнимой upgrade: завести openspec,
привести config.yaml к скелету, вычистить из context пересказ, проверить имя
файла, поднять номер в .pm.json. Проверка прогнана на четырёх фикстурах — свежий
openspec init, два живых проекта и пустой каталог; отличает все четыре случая.
Решение — 47.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 11:59:01 +03:00
avandClaude Opus 5 91d4264b40 правило выбора метки уехало в свой документ, в скилле остался диспетчер
SKILL.md конвейера дорос до 1168 строк, и двести с лишним из них отвечали на
вопрос, который на обычной задаче не задаётся: как выбирается метка. Называет её
review-scope один раз, до обеих стадий, а всем остальным нужна не процедура
выбора, а состав по уже названной метке — три строки таблицы. В
references/review-levels.md переехали правило двух осей, «спорное решается вниз»,
«максимум по поверхности», разбор того, чем small дешевле medium, и обе проверки
долей. В скилле остались таблица состава, схема процесса и раздача тем: метка
названа — состав читается.

Форма выбрана одна на все метки, а не по документу на метку, как у типов задач в
av-dev-pm:tasks. Аналогия не переносится дважды. Типы задач разъединены — общее
лежит в task-format.md, в файле типа только своё; метки вложены: medium это small
плюс два прохода, large — medium плюс доказательство, и три файла повторяли бы
костяк трижды. Такое расхождение copies.py не ловит: он сверяет дословные копии
по маркерам, а вышли бы почти-копии с намеренными мелкими отличиями, неотличимые
от задуманного. Причина сильнее: ценность текста в сравнении. Вопрос читателя не
«что делает small», а «чем small отличается от medium» — на него отвечают и выбор
метки, и «спорное вниз», и корректор; сравнение, разложенное по трём файлам, не
читается.

Механика рычагов осталась в скилле. Непуск, вход и потолок общие для всех
проходов и всех меток, их дом — «Модель по проходу»; в переехавшем тексте от них
только то, что они делают с small, и ссылка на дом. Точные потолки не
продублированы, чтобы не заводить второй источник чисел.

Ссылку на дом правила получили review-scope, для которого он основная опора, и
task-pipeline, где раньше стояло безадресное «правило живёт в скилле конвейера».
Заодно вычищено последнее живое упоминание quick и standard: имена удалены
каноном 6, но уцелели в объяснении, зачем нужна проверка доли.

Решение — 46. SKILL.md: 1168 → 1026 строк.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 11:40:09 +03:00
avandClaude Opus 5 9561af7b9b корректор метки переехал в code; размер считается по пяти источникам
Сигнал «метка, вероятно, занижена» жил в review-basics — в единственном месте. А
basics с меткой small не запускается, если у проекта нет своих тем: значит на
типичном проекте задача с меткой small шла без рантайм-проверки того, что метка
выбрана верно. Дыра появилась вместе с удешевлением small и попала в самую
вероятную точку ошибки: занижают туда, где дешевле, а цена занижения там же и
выросла — три темы ядра смотрятся только против записанных инвариантов.

Сигнал перешёл в review-code, и он подходит по построению: идёт при любой метке,
видит дифф целиком, а на small уже читает инварианты, то есть держит весь
материал, из которого сигнал выводится. Признаков четыре, и один весит больше
прочих — изменение, которое не откатывается обратной правкой, при метке small это
прямой промах отрицательного теста. У basics сигнал остался вторым,
подтверждающим: он смотрит оптикой тем и видит то, чего не видно из кода как
кода, — что вопросов, отложенных до large, накопилось слишком много. Триаж теперь
обязан сказать и когда сигнала нет: «корректор отработал, возражений нет» и
«корректор не запускался» по молчанию неразличимы.

У small появилась доля, и она сформулирована сравнением, а не порогом: small не
должен обгонять medium, ориентир — до трети задач. Проверка нужна именно теперь.
Пока quick и standard совпадали составом, дрейф между ними не стоил ничего, и её
не было; сейчас он стоит трёх тем ядра. У дрейфа вниз есть стимул, и он назван
прямо: метку выбирает не автор, но по описанию, написанному автором — занижённое
описание даёт занижённую метку без чьего-либо умысла.

Размер теперь считается по корпусу из пяти источников. Разметчик читал
proposal.md и tasks.md, но design.md не открывал вовсе, а метод был описан одной
фразой «размер считается по дельта-спекам». Дельты описывают заказанное поведение
и молчат об объёме работы: шесть шагов в двух узлах видны в tasks.md, а факт, что
форму решения выбирали из нескольких, — только в design.md. Каждый источник
получил свою строку по каждой оси, и каждая цифра обоснования обязана быть
привязана к источнику поимённо; «изменение выглядит средним» обоснованием больше
не считается.

Отсюда два правила, которых не было. Расхождение источников по объёму
разрешается в пользу большего — и это не «спорное решается вниз»: то правило
разрешает ничью при равных данных, а здесь один источник просто видел больше.
Само расхождение при этом идёт доводом за незнакомое: если о задаче написано так,
что источники не сходятся в объёме, форму решения по ней не знают. Отсутствие
design.md у нетривиальной задачи читается так же — «форму знали заранее» ничем не
подтверждено.

Заодно две грамматические опечатки от вчерашнего переименования в SKILL.md.
Решение — 45.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 11:08:54 +03:00
avandClaude Opus 5 d5bee11a6b классификация задачи: три категории документов и метка вместо ступени
Канон 5 объявил «каждый документ docs/ — тема ревью». Правило верно ровно
наполовину и потому вредно целиком. Паспорт и схему хранилища ревью читает, но
темами они не являются: по ним нельзя сказать «в этом изменении сделано не так»,
они задают границу, по которой судит чужая тема. Журнал решений и журнал
наблюдений ревью изменения не нужны вовсе — ADR объясняет прошлое, а не
предъявляет требование. Разметчик, применявший правило буквально, обязан был
либо завести фантомные темы passport, adr, database, research и продублировать
ими работу architecture и operations, либо потерять четыре документа молча;
случались обе ветки, и в собственном образце плана docs/passport.md не попадал
ни строкой, а обязательная арифметика покрытия при этом не сходилась.

Категорий теперь три, разрез проверяемый. Тема — да, прямо: conventions,
security, architecture и любой свой документ проекта. Источник темы — нет, но он
задаёт границу для чужой: passport, database, CLAUDE.md, openspec/specs.
Процессный — нет, он про то, как мы работаем: tasks, review, adr, research,
.pm.json. Открыта одна категория из трёх, две другие перечислены поимённо, так
что документ вне раскладки — однозначно своя тема. adr и research прогон больше
не открывает ни одним проходом; docs/review остаётся читаемым, но как настройка
конвейера, а не критерий. Цена записана и стала обязательной строкой границ
покрытия: расхождение с записанным решением ловит теперь только сверка
документации, а число под находкой обязано быть снято на этом прогоне, с
приложенной командой.

Классификация выдаёт задаче метку — small, medium, large. Прежние quick,
standard и wide назывались ступенью и описывали ревью: как глубоко смотрим.
Классифицируется же задача, и пока величина называлась свойством прогона, её
естественно было пересчитывать на каждом прогоне — что конвейер и делал. Слово
«ступень» удалено, а не оставлено синонимом: два имени одной вещи расходятся.
Выводится метка из двух разведённых осей — размер (малое, среднее, крупное) и
сложность (знакомое, незнакомое), — и равна максимуму по ним. Метка не синоним
размера: малое незнакомое изменение получает large, трогая один узел, поэтому
план печатает три строки с обоснованием каждая и выводить одну из другой
запрещено. Оси остались русскими словами — это суждение прозой; метка
английская — это идентификатор, который проходы сравнивают.

Разметка переехала из ревью кода в шаг 4 пайплайна, сразу после propose. Она
шла первым проходом каждого ревью кода, а перед ревью дизайна ту же величину
называл сам пайплайн — то есть оркестратор, который только что довёл
предложение до propose. Одно и то же измерялось дважды, и один из двух раз без
разведённости с автором, ровно в той точке, ради которой разметчик заведён.
Теперь запуск один на задачу, диффа он не видит, план обслуживает обе стадии, и
метка после кода не пересматривается: расхождение факта с разметкой ловит журнал
дефектов постфактум, как и всякую другую ошибку выбора. На диск план не пишется —
четвёртый артефакт рядом с proposal, tasks и design пережил бы задачу и разошёлся
бы с ней молча.

Ревью дизайна тоже растёт меткой: small — specs, medium — плюс rubric, large —
плюс architecture и вопрос автору о трёх формах решения. Раньше rubric и
architecture включались одним условием, и medium получал ровно один проход, то
есть не отличался от quick ничем. Разведены они потому, что зарабатывают на
разном: рубрика порождает свойства узла и окупается уже на среднем изменении,
её выход уезжает приёмочными критериями в tasks.md; архитектура отвечает на
вопрос про второй способ, а он на среднем знакомом изменении отвечается «нет»
ещё до запуска.

small подешевел тремя способами сразу. Составом: приёмник тем не запускается,
три темы ядра переходят к code сверкой по записанным инвариантам CLAUDE.md с
потолком в одну находку, и это не «глубина ниже», а другой дом темы. Входом:
specs читает только дельта-спеку, code — только индекс конвенций. Потолком: он
появился у каждого опиниативного прохода, а не у одного basics, и у половин code
он раздельный, потому что конвенционных находок больше по построению и в общем
списке они вытеснили бы техническую половину. Сработавший потолок обязан быть
объявлен строкой — молчащий срез неотличим от «больше не нашлось». Отрицательный
тест small от этого стал жёстче, а не мягче: вопросы про обратимость миграции
задавал приёмник тем, и на этой метке их не задаст никто.

Пайплайн задачи вырос до двенадцати шагов. Тривиальность перестала решать состав
ревью — она влияет только на explore; глубину обеих стадий называет метка.

Проверено прогоном ревьюверов по готовому результату: девять расхождений найдено
и починено — контракт находок печатал старый перечень проходов вместо плана по
темам, три ссылки в task-batch указывали на шаг коммита вместо закрытия, запись
changelog не переводила вопросы, адресованные passport и database, ops и
adversary утверждали, что на нижних метках их вопросы задаёт basics, шаблон
покрытия в review-code зашивал потолки small намертво, триггеры метки рассыпались
на два списка против трёх, тема из директивы CLAUDE.md могла остаться без запуска
исполнителя. Гейт зелёный: фронтматтеры, копии, одиннадцать диаграмм, ruff,
pyrefly; docs.py прогнан на живом фикстуре и печатает категорию в отказе.

Канон повышен до версии 6 с записью, выполнимой upgrade. Решения — 40–44.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 10:57:15 +03:00
avandClaude Opus 5 61cd9fcd37 гейт судит staged-файлы; рендер диаграмм пошёл параллельно
Гейт проверял рабочее дерево целиком — то есть не то, что уедет в историю, а
то, что лежит на диске рядом. Плюс платил за это временем: пятнадцать секунд на
каждый коммит с правкой markdown, потому что одиннадцать блоков рендерились по
очереди, каждый своим запуском mermaid-cli со своим chromium.

diagrams.py научился двум вещам. Первая — принимать файлы списком: без
аргументов обходит репозиторий как раньше, с аргументами смотрит только
названные, отбирая из них markdown внутри корня (гейт передаёт весь staged, где
есть и скрипты, и удалённое). Вторая — рендерить пулом потоков: работа целиком в
ожидании подпроцесса, своего интерпретатора ей не надо, а потолок в восемь
воркеров упирается в память chromium, а не в двадцать четыре ядра. Порядок
находок берётся из порядка сбора, не из порядка ответов, так что вывод
детерминирован. Весь репозиторий — 3 секунды вместо 15, один файл — 1.

В хуке теперь {staged_files} у диаграмм, ruff и pyrefly. Два исключения
остались, и оба по существу: copies.py сверяет копию с домом, а дом лежит в
другом файле, которого в индексе может не быть — список staged дал бы «копии
дословны» ровно там, где правка дома их и разошлась; frontmatter.py обходит всё
за сотые доли секунды, экономить нечего. Оба объяснены прямо у своих задач.

ruff встал с --fix и stage_fixed: безопасное чинится само и доносится до этого
же коммита. Иначе исправленный файл оставался бы в рабочем дереве, а в историю
уезжал бы невычищенный — гейт зелёный, коммит грязный.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 09:30:56 +03:00
avandClaude Opus 5 5067bc2048 гейт коммита: пять машинных проверок встали в pre-commit
Проверки существовали и запускались руками — то есть тогда, когда о них
вспоминали. Каждая из них ловит ровно тот класс поломок, который не виден при
чтении: фронтматтер разбирает загрузчик скиллов, а не человек; копия правила
расходится с домом молча; диаграмма mermaid выглядит правдоподобно и падает на
рендере; ruff и pyrefly стерегут ноль внешних зависимостей, без которого
tasks.py и docs.py перестают работать в чужом проекте. Полагаться на память в
таком наборе — значит узнавать о поломке от того, кто скачал плагин.

Ставится lefthook (конфиг в lefthook.yml, `lefthook install` один раз на клон),
пять задач в parallel. Glob разводит две половины: правка одних скриптов не
платит за рендер диаграмм (~15 секунд), правка документов не гоняет линтеры.
Внутри своей половины проверяется весь репозиторий, а не изменённые файлы — и
расхождение копии, и находка ruff в соседнем файле это ровно тот случай, когда
правка сломала не себя.

Два свойства названы в README и в шапке конфига, чтобы не выяснялись отладкой.
Первое: судится рабочее дерево, а не индекс — скрипты написаны как обход
репозитория и про git add не знают, поэтому частичный коммит при грязном дереве
проверяется по тому, что на диске. Второе: обход разовый — LEFTHOOK=0, и он
законен ровно для случая, когда найденное нечем чинить прямо сейчас.

Заодно поправлена строка README про диаграммы: «поэтому он не в гейте, а в
руках того, кто правит диаграмму» — с этой правкой она перестала быть верной.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 09:24:37 +03:00
avandClaude Opus 5 37394444e8 спринт без цели: цель стала необязательной, но не молчаливой
Цель была обязательной во всех трёх местах сразу: sprint start требовал слаг,
check считал ошибкой набор без названной цели, sprint take отказывал задаче под
чужой целью. Модель описывала только спринт развития — а спринт бывает под
багфикс, под техдолг, под здоровье проекта: такой набор собран по
работоспособности, а не по направлению, и цели у него нет не по недосмотру.

Обходной путь существовал и был хуже прямого: завести цель-пустышку «Здоровье
проекта» и вешать под неё fix-ы. Тогда ROADMAP.md — документ про то, что
приложение умеет, — обрастает строками про то, что оно не ломается, а тег goal:
перестаёт значить направление.

Теперь sprint start принимает --goal <слаг> ИЛИ --no-goal, и голое отсутствие
обоих — отказ с объяснением. Причина в стимуле: цель называет человек, и это
единственный продуктовый вопрос всей сессии. Разреши мы заводить спринт просто
без флага — забытый флаг, лень спросить и осознанное решение стали бы
неотличимы на выходе, а дешевле всего из трёх агенту именно не спрашивать. Тот
же обход записан в «Стимулы» скилла session вместе с защитой.

В спринте без цели цель не проверяется вовсе: набор берёт что угодно готовое к
взятию, включая задачи под разными целями. Правило «набор служит одной цели» не
ослаблено, оно просто не применяется — целей там не больше одной, их ноль.
Взамен машинной проверки остаётся показ набора человеку до заморозки, и в
cadence.md сказано прямо: у бесцельного спринта это единственная проверка
состава. Доклад обязан называть спринт бесцельным и объяснять, чем он был.

Признак «спринт идёт» разъехался с целью и переехал на слаг: слаг есть у любого
спринта, потому что без него нечем проставить sprint:<слаг>, то есть нечем
собрать урожай. На sprint_started() переведены sprint take, блок здоровья check
и reopen. Поле «Цель» в шапке остаётся и у бесцельного набора — пишется прозой
без ссылки: «цели нет» и «цель потерялась» обязаны различаться.

Побочно выправлен reopen: он возвращает задачу в набор идущего спринта тем же
тестом, что и sprint take (мешает только чужая цель). Прежде тест был уже —
совпадение целей, — и задача без цели, реопенутая при спринте с целью, уезжала
в беклог вопреки прозе скилла.

Тема 39 в DECISIONS.md, следствия 145-147.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 09:19:29 +03:00
avandClaude Opus 5 4386eb3e1c шов между плагинами: канон перестал называть имена проходов
av-dev-pm и av-dev-pipeline раздельны: канон работает без конвейера, конвейер без
канона — поразрядно деградируя и называя это строкой. Но канон в шести местах
называл конвейер поимённо, и одно из них — вывод docs.py пользователю: «свои темы
проекта: … — их разбирает review-basics». Такая строка чинится не правкой файла,
а недоумением на чужом проекте.

Правило записано в canon.md, чтобы не отрастало заново. Общий словарь — имена тем
и имена ступеней, и только они: ими проект настраивает ревью, вопросами по темам
и триггерами профиля. Имён проходов канон не называет нигде. Направление
несимметрично, и это верно: конвейер называет документы канона поимённо, потому
что он их читатель, а обратной ссылки быть не может — документ живёт дольше, чем
раскладка проходов.

Что вычищено: имя review-basics в canon.md, в changelog версии 5 и в выводе
docs.py; «её берёт basics» из таблицы ролей; описательные адресации того же
класса — «архитектурный проход судит», «враждебный проход выдумает». Худшей была
строка в skeletons.md «там идут враждебный, эксплуатационный и архитектурный
проходы»: утверждение о составе ступени, живущее на стороне, которая о составе не
знает. Строка таблицы «эксплуатационный проход ревью» стала «тема ревью
operations» — заодно совпала со словарём, к которому tasks/SKILL.md отсылает как
к единому дому.

Отдельно — пример, нарушавший собственное правило. Объяснение, почему вопросы
адресуются темам, звучало «вопрос, адресованный ops, перестал задаваться в тот
день, когда ops уехал в верхнюю ступень»: правило про нестабильность имён,
проиллюстрированное именем. Стало «адресованный проходу» и переживёт
переименование.

Починена и висячая ссылка: project-facts.md отсылал к таблице «Кто читает» в
каноне, которой там нет — она была убрана правкой, вводившей темы, и по новому
правилу её и не должно быть. Списка читателей не ведёт никто: читателя назначает
план прогона.

Тема 38 в DECISIONS.md, следствия 143-144.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 08:57:09 +03:00
avandClaude Opus 5 900f3f83ca gate и autotests сведены к одному имени
Тема звалась autotests, а закрывающий её проход — gate, и на всех трёх ступенях
это была одна и та же клетка таблицы. Одна сущность под двумя именами — та же
ошибка, что и два разных под одним, только тише: она не путает, а теряет. Вопрос
проекта в docs/review адресуется теме; адресованный проходу не приезжает никуда,
и ровно этот отказ уже случился однажды с ops.

Победило имя темы. Тема первична по правилу 0, а имена тем — это имена
документов: docs/autotests.md проект напишет (что покрыто, что нарочно нет, где
testdata), docs/gate.md не напишет никто, потому что гейт это команда, а не
предмет. Слово «гейт» к тому же занято дважды — команда проекта и ребро графа;
третьим значением стал бы нечитаемым отчёт, где «гейт красный» и «гейт нашёл»
про разное. И тема шире гейта ровно на «чего в гейте намеренно нет».

Цена названа честно: autotests звучит уже своего содержимого — линт, типы и
сканер уязвимостей тестами не являются. Гасится строкой в уставе: тема — это
«проверено ли машиной», а не «есть ли тесты», гейт в ней инструмент, а не
граница.

Слово «гейт» осталось ровно в одном значении — команда проекта. Все прочие
вхождения (семантика гейта, «пока гейт красный», финальный гейт в task-batch)
именно про неё и не тронуты.

Побочно: autotests — единственная тема, чей дом лежит не в docs/, а в CLAUDE.md.
Канон править не пришлось: список тем открытый, и заведённый когда-нибудь
docs/autotests.md ляжет на существующее имя.

Тема 37 в DECISIONS.md, следствия 141-142.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 08:50:13 +03:00
avandClaude Opus 5 a81dd1a5a7 ревью по темам: документ проекта стал направлением проверки
Замечено при сверке документов канона с составом ступеней: три документа
остались без читателя ниже wide — security.md, database.md и adr/. Проект
поддерживал их, а на 90% задач не открывал никто. Причина оказалась не в
переезде проходов, а в том, как описан состав прогона.

Список тем нигде не был записан: он существовал побочным продуктом списка
проходов. Проход уезжал в верхнюю ступень — и тема уезжала с ним беззвучно.
Отчёт честно говорил «ops не запускался» и не говорил «эксплуатацию не смотрел
никто», а нужно второе. Теперь тема первична, проход вторичен — это правило 0
конвейера, а прогон описывается таблицей «тема → дом → глубина → кто закрывает»,
и таблица есть в каждом отчёте.

Тема есть документ, список открытый. Всё, что проект кладёт в docs/, становится
темой ревью; запретить нельзя, разрешения не надо. Не темы ровно две: docs/tasks/
и docs/review — настройка самого конвейера, слой над темами. Отсюда главное:
docs/ перестал быть документацией и стал конфигурацией конвейера. Проект
настраивает проверку тем, что пишет о себе, а не отдельным файлом настроек,
который разошёлся бы с документами. Ядро — requirements, autotests, conventions,
architecture, security, operations; всё сверх разбирает basics, потому что
именных проходов конечное число, а тем столько, сколько заведёт проект.

Тема живёт файлом или каталогом, на выбор проекта: docs/security.md и
docs/security/ — одно и то же. Прежде форма была задана поимённо и обосновать её
было нечем; заодно в TODO висел вопрос «а если architecture.md разрастётся».
Теперь ответ механический: разросся — стал каталогом с README.md, и это не смена
версии. Обе формы сразу — ошибка, docs.py её ловит.

Заведён review-scope, sonnet, стадия 0, до гейта: находит документы, выводит
темы, назначает глубины, выбирает ступень с обоснованием. Довод оказался сильнее
синхронизации документов — до сих пор профиль называл тот же оркестратор,
который написал код, то есть в точке выбора глубины проверки разведённости с
автором не было вовсе, а решала она под давлением «я почти закончил». Вызывающий
пайплайн профиль больше не передаёт. Поднять и понизить ступень разметчик вправе
одинаково, но обоснование обязательно всегда.

Sonnet ему хватает потому, что вывод устроен как список: каждый файл в docs/
обязан попасть в план темой или строкой «не тема, потому что», и план сверяется с
ls docs/ за секунду. Выбор ступени — суждение, но у него три независимых
корректора: отрицательный тест quick, правило «спорный случай вниз» и сигнал
basics о заниженной ступени.

Разметчик передаёт адреса, а не пересказ. Проект однажды уже держал
review-brief.md и убрал его: второй дом расходится с первым и выглядит
актуальным. Пересказ в задании — тот же посредник, живущий один прогон.
Исключение одно: отсутствие дома, этого проход сам дёшево не выяснит.

quick и standard совпали составом и разошлись глубиной — иначе требование
«нижние ступени закрывают все темы, просто не так глубоко» не выполняется.
Глубин три, и они про способ доказательства, а не про старательность: сверка
(открыть дом, открыть дифф, сравнить), разбор (построить сценарий рассуждением),
доказательство (прогнать, померить, построить путь). Третья есть только в wide.
Цена принята: это единственное место, где профиль не выводится из списка
проходов, поэтому глубина объявляется в отчёте наравне со ступенью.

review-code переписан, и это оказалось крупнее исходной находки: код как код не
читал никто. specs сверял с требованиями, basics — с отказами окружения,
architecture — с устройством, а code был проходом только по прозаическим
конвенциям и прямо объявлял, что рантайм и логика не его. «Здесь ошибка в логике»
не говорил вообще никто. Теперь у прохода две половины: девять классов
технического дефекта (необработанная ветка отказа, пустое и нулевое, граница
диапазона, перепутанный операнд, неосвобождённый ресурс, изменение под итерацией,
неверно применённый интерфейс библиотеки, недостижимая ветка, «сделано соседнее»)
и прежняя сверка с конвенциями. Модель поднята до opus по признаку темы 35: цена
пропущенной находки — дефект в проде.

Канон повышен до версии 5: форма дома на выбор, открытый список тем, AGENTS.md
законно лежит рядом с CLAUDE.md, «Вопросы к проходам» → «Вопросы по темам» (имя
прохода переезд не переживает, тема переживает), «Недоступно проверке» — тоже по
темам. docs.py переписан под темы: ловит двойной дом, принимает обе формы,
перечисляет свои темы проекта вместо «файл вне канона».

Побочно закрыт давний пункт TODO про каталожную форму architecture.md — решать
больше нечего.

Прогон от всего этого стал дороже, а не дешевле, впервые за сессию: плюс scope в
голове каждого прогона, плюс code на opus, плюс basics теперь и в quick. Куплены
разведённость выбора ступени, видимость непокрытых тем и технический разбор кода,
которого не было вовсе.

Тема 36 в DECISIONS.md, следствия 137-140.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 08:35:11 +03:00
avandClaude Opus 5 c93a9d1269 модели субагентов: двое из девяти опущены на sonnet, критерий переписан
Сквозной проход по тринадцати уставам с одним вопросом: кого из девяти
opus-агентов можно опустить без потери. Ответ — двоих, и оба не из конвейера.

По дороге выяснилось, что критерий, которым модели раздавались до сих пор,
отвечает не на тот вопрос. Деление applicative против generative смотрит, откуда
проход берёт критерий, а платит проект за разбирательство с находкой. Рабочий
признак другой: находка со ссылкой на записанный источник — строку спеки, цель в
манифесте, значение в конфиге — опровергается открытием файла, и дешёвая модель
ошибается здесь проверяемо; находка-суждение опровергается рассуждением, а
рассуждение стоит триажа или человека. Признак объясняет и прежнюю раскладку
лучше, чем она сама себя: gate, code и ops дёшевы не потому, что у них чек-лист,
а потому что каждая их находка показывает пальцем на строку.

doc-code-drift переведён на sonnet. Закрытый перечень из восьми правил, каждое —
пара «факт в документе ↔ команда, которой он проверяется». Устав прямо запрещает
суждение, требует формы «написано X, в коде Y, проверено командой Z» и правила
«нечем проверить — не находка». Ложная находка опровергается той же командой,
которая её породила.

task-form переведён на sonnet, и решило не устройство, а потребитель. Его находка
это готовая формулировка, которую человек читает и отклоняет командой edit, а не
оркестратор, который молча реализует всё прочитанное. Довод, державший triage
наверху, здесь не работает вовсе: ошибка стоит строки чтения.

review-specs рассмотрен всерьёз и оставлен на opus — по причине, обратной общей.
Он самый частый opus-проход, идёт и в design, и на коде. По устройству
applicative: SKILL.md сам называл стадию 1 «два applicative-прохода, оба
дешёвые», платя за одного sonnet, за другого opus. Расхождение закрыто текстом, а
не переводом. Наверху его держит направление code → spec, где надо заметить
отсутствие: тихий фолбэк, самодеятельный дефолт, проглоченную ошибку. Прочие
держат opus из-за цены ложных находок, этот — из-за цены пропущенных, а пропуск
не оставляет следа ни в отчёте, ни в границах покрытия.

Остальные шестеро оставлены с причиной у каждого: adversary и rubric порождают
критерий по построению, architecture — чистое суждение о структуре, triage —
сток, doc-consistency путал бы «упомянуто в двух местах» с «оба утверждают»,
basics заведён этой же сессией и половина его вопросов суждение.

Это разбор уставов, а не замер, и так и записано. calibration.md двигает модель
инъекцией дефекта; инъекции не было. Двое переведены потому, что цена их ошибки
ограничена сверху независимо от модели.

Поправлена проза, ссылавшаяся на прежние модели: «оба судьи на opus» в cadence,
canon/SKILL, canon.md и docs/SKILL — теперь дорог по-настоящему один
doc-consistency, второй читает репозиторий целиком, но идёт на sonnet. В
tasks/SKILL снято «отсюда и разные модели»: у task-form и doc-wording она теперь
одна, а разрез по глубине остался.

Тема 35 в DECISIONS.md, следствия 134-136.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 19:36:02 +03:00
avandClaude Opus 5 21b840a8e4 профили ревью: тяжёлые проходы в верхнюю ступень, на умолчании — один базовый
Тема 33 сняла самую большую разовую статью расхода, но не тронула главную —
частоту. Меряющая пара стояла в standard, то есть на большинстве задач, и именно
она делала прогон долгим: два прохода держат машину, идут цепочкой и доказывают
находки запуском. Цель разбора названа прямо: лучше поправить в следующей задаче,
чем держать одну два часа.

adversary и ops переехали в wide. Стадия осталась самой урожайной за всю историю
замеров — пять из семи выживших находок дозапуска и единственная находка про
молчаливый старт отката, — но её ценность оплачивается на каждой задаче, а
получается на немногих. Решение по цене, не по ценности.

Заведён review-basics: мелкая осадка двух тяжёлых проходов, без единого запуска.
Стоит только в standard. Восемь вопросов, на которые отвечают чтением: таймаут и
отказ соседа, идемпотентность и одновременная запись, остановка на середине,
частичный откат при двух версиях, наблюдаемость и тишина, очевидный рост объёма,
второй способ мимо единой точки (грепом, не картой), что отсюда удалить. Потолок
4 находки, машину не держит, ничего не меряет.

Вопрос про частичный откат — не для полноты списка. Без него правило «миграция
схемы не поднимает ступень» рассыпалось бы: раньше миграцию разбирал ops, а он
теперь наверху. Проход заведён затем, чтобы у standard остался хоть один взгляд
на ось времени.

Модель у него верхняя, opus, и это не спорит со словом «средний»: усилие режется
входом и потолком, а не моделью. Дешёвая модель на опиниативном проходе платит
триажем — это записанный замер, отменять его без нового замера нечем.

Лестница вышла 4/5/7. Главный выигрыш не в числе проходов, а в том, что из
standard ушла цепочка: теперь там гейт, три прохода одним сообщением и триаж —
граф плоский, ждать некому.

Правило выбора ступени переписано на два вопроса, и объём изменения вошёл в него
впервые. Крупное или незнакомое — трогает несколько узлов, переносит
ответственность, форму решения нащупывают по ходу — это wide, и он рассчитан на
5-10% задач. Мелкое — один узел, форма очевидна заранее, откат сводится к
обратной правке — quick. Всё остальное standard, рабочее умолчание. Раньше
ступень выбиралась только по классу изменения и на размер смотреть запрещала;
теперь признаков два: класс отвечает за обратимость, объём — за цену
разбирательства.

Отрицательный тест сохранил прежнюю мудрость в новой рамке: что после мерджа не
откатывается обратной правкой — не quick, каким бы маленьким ни был дифф. Три
строки миграции идут в standard.

Спорный случай решается вниз, и асимметрия объяснена ценой: ошибка в сторону
standard стоит находки на следующей задаче, ошибка в обратную — трёх тяжёлых
проходов на каждой задаче, выбранной неверно.

Сделка записана вместе с обратной связью, иначе это тихая потеря качества. На
quick и standard не проверяется ничего, что требует запуска: построенный путь,
эксперимент против драйвера, любое число. Это самая крупная граница покрытия
конвейера, и она идёт строкой в каждом таком прогоне поимённо. Сигналов о том,
что ступень занижена, два: журнал дефектов в docs/review.md и сам basics —
он единственный, кто смотрит на дифф целиком на нижних ступенях, и обязан
сказать строкой, если задача выглядит крупнее профиля.

Побочно: условие профиля design то же самое, так что rubric и architecture на
предложении тоже упали до 5-10% задач.

Тема 34 в DECISIONS.md, следствия 130-133. Версия канона не поднята; инструкция
проекту дописана в пункт 8 записи «Версия 4».

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 19:29:07 +03:00
avandClaude Opus 5 ea84a4fbb3 стоимость ревью: снят проход независимой реализации и самая дорогая модель
Прогоны стали долгими, а счёт в токенах заметным. Разбор шёл не по находкам, а
по статьям расхода. Две названы прямо: убрать reimpl и убрать fable.

reimpl писал свою реализацию узла, не открывая существующую, и диффил по
решениям. Его счёт определялся объёмом вывода — он один писал код, а не читал
его, — и на прогоне это была самая большая строка. Снят по цене.

Профиль deep от этого не похудел, а исчез: reimpl был единственным, чем он
отличался от wide, и без него у двух имён оказался бы один состав. Ровно от
этой болезни лечилась ступень wide решением JJJ — у профиля обязан быть один
правильный ответ, иначе реестр состава нечем проверять. Ступеней три: quick,
standard, wide.

Вместе с профилем снято всё, что обслуживало только его. Барьер стоимости —
он держал дорогой проход, чтобы тот не писал реализацию против кода, который
через час перепишут; дорогого прохода нет, граф стал плоским во всех профилях,
рёбер осталось два вида вместо трёх. Тест «идентичность, слияние, разбор» —
полторы страницы, служившие единственной цели: выбрать deep не по ощущению;
вместе с ним ушёл проектный перечень мест в docs/review.md и его скелет в
каноне. Стадии перенумерованы: 0 гейт, 1 сверка, 2 враждебный и
эксплуатационный, 3 архитектурный, 4 триаж — дыра на месте третьей читалась бы
как пропущенная стадия.

Снятие записано как сознательное сужение, а не как «класс оказался пустым».
calibration.md требует замера на двух проектах перед удалением прохода; замера
не было, было решение о цене. Поэтому в «Честном пределе» стоит строка: «не
знаю, чего не знаю» больше не достаёт никто. Остаток независимого взгляда дают
профиль design и architecture, но альтернативной реализации, с которой можно
сдиффить решения, у конвейера нет. Класс уходит в границы покрытия каждого
прогона, у проекта — в подраздел «перестали проверять сознательно». Без этой
записи снятие через месяц читается как «проверено и признано лишним».

fable снят с троих: review-triage, review-architecture, doc-code-drift — все на
opus. Основание верхней модели «ошибка распространяется дальше самой находки»
осталось, но оно объясняет, почему двое не опускаются до sonnet, а не почему им
нужна ступень выше opus: разницы в пользу более дорогой модели не показал ни
один прогон, а время и счёт она множила. Палитра схлопнулась до двух цветов,
красного в репозитории больше нет, frontmatter.py теперь отвергнет модель вне
sonnet и opus.

Версия канона не поднята сознательно. Проектам всё равно надо снести перечень
мест для deep из docs/review.md, поэтому пункт вписан в «Что сделать проекту»
записи «Версия 4» — её ещё не гонял ни один проект, оба ждут в TODO.

Тема 33 в DECISIONS.md, следствия 127-129.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 19:02:15 +03:00
avandClaude Opus 5 cbfae90f3f словарь: пять слов сняты, девять закрыты списком вместо оговорки «прижилось»
Проход упрощения уткнулся в один класс у всех пяти агентов: слово, живущее в
трёх-шести файлах разом. Правка в одном месте развела бы словарь, правка во
всех — уже не упрощение текста скилла. Каждый честно остановился и записал слово
в отчёт, и одни и те же слова всплыли в разных отчётах. Разобрано этим проходом.

Причина, по которой они вообще накопились, оказалась в самом уставе языка. Он
разрешал не переводить «термин, у которого нет точного русского эквивалента и
который в команде уже прижился». Проверить это нельзя: прижившимся выглядит
любое слово, встреченное трижды, — и ровно так рассудили пять агентов подряд,
каждый независимо. Оговорка заменена закрытым списком из девяти терминов с
колонкой «что называет»: интейк, триаж, провенанс, дедуп, чек-лист, дифф,
промпт, сущности OpenSpec, роды проходов ревью. Интейк оставлен потому, что
«заведение» называет создание файла, и слить их значит смешать две операции;
провенанс — потому что «источник» рядом называет саму запись, а не свойство
числа. Слово не из списка и не из таблицы имён вещей — находка, а не стиль.

Список заведён домом язык-словарь в language.md и копией в уставе doc-wording.
Копия обязательна: агент работает в репозитории проекта, где плагина может не
быть, и без списка предъявил бы интейк как англицизм.

Снято пять слов, 29 мест: конфляция → смешение, декорреляция → разведённость,
непоймание → почему не поймали, эвал-сет → проверочный набор, гайд →
руководство. Латинизм или калька при живом русском слове в каждом случае.

Разбор декорреляции показателен: проект уже владел нужным словом — «агенты
разведены по глубине», «разведены по охвату» — и держал рядом латинский синоним
того же понятия. Это не англицизм, а второй дом для слова.

Непоймание снято ещё и потому, что форма журнала дефектов, которую канон кладёт
в проекты, спрашивает «Почему не поймали», а проза рядом называла это «причиной
непоймания». Скелет и проза о скелете говорили разными словами.

Снятое записано вместе с оставленным, в одном списке и с заменой каждого. Иначе
слово возвращается: из текстов оно уходит, но ничто не мешает следующему проходу
завести его заново — оно ведь короткое и точное на вид.

Тема 32 в DECISIONS.md, следствия 124-126. Нумерация правил в уставе doc-wording
сдвинута: словарь встал шестым, жаргон и далее уехали на единицу.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 15:35:06 +03:00
avandClaude Opus 5 47a2f3de63 язык скиллов av-dev-pm: проход упрощения пятью агентами и разбор находок
Эксперимент: по сабагенту на каждый скилл av-dev-pm, задача — переписать текст
более простыми словами, но только там, где уверен и без потери смысла и
точности. Нормой служил устав языка самого проекта, language.md, включая его
раздел «Порог правки»: правка без нарушенного правила не делается.

36 правок в двенадцати файлах, +64/-63 — почти строго замена, а не
переписывание. Правили залог (пассив с названным деятелем в творительном),
отглагольные существительные, параллельность перечней, канцелярит «является»,
пару garden-path и одно двойное отрицание. Контракт не задет нигде: в диффе нет
изменённых строк-заголовков, а код-спаны встречаются ровно парой минус-плюс,
то есть ни имя, ни флаг, ни путь не переписаны.

Отчёты «что рассматривал и не тронул» вышли длиннее отчётов о правках у всех
пятерых, и это главный результат прогона. Самый частый повод остановиться —
слово, живущее в четырёх файлах: конфляция, интейк, провенанс, непоймание.
Правка в одном месте развела бы словарь, а править все — уже не упрощение
текста скилла, а сквозной проход по репозиторию. Второй повод — формулировка,
дословно повторённая в соседнем плагине: декорреляция, материализация
нерешённого, «при расхождении прав текст».

Шестой агент проверил все 36 правок и нашёл четыре.

Перестановка слов в task-research.md развела формулу с её домом: «число без
источника проход ревью обязан читать как условие» стоит в canon.md и в уставе
doc-consistency, который прямо ссылается на канон как на источник. Откачено —
это ровно тот класс расхождения, который сам doc-consistency и ловит.

«Держит H1, мету и индекс в согласии» — управление требует дополнения, а
language.md в разделе англицизмов прямо оговаривает: русский аналог звучит
коряво — остаётся термин. Взят третий вариант, «согласованными».

В skeletons.md «правка тянет запись, и она называет» — местоимение указывает
на два женских существительных сразу. Стало «и запись называет».

Четвёртая находка не откачена: правка в init/SKILL.md хорошая, но развела
конструкцию с близнецом в canon/SKILL.md — выровнена вторая половина.

Побочно найдена старая логическая инверсия в DECISIONS.md, решение U:
«становится неотличимым, только если отрицание обязательно» — смысл вывернут,
в docs/SKILL.md и во второй записи журнала он правильный. Починено.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 14:49:03 +03:00
avandClaude Opus 5 2d39a77444 ревизия покрытия av-dev-pm: три решения из шести оказались «убрать»
Сабагент в роли продакт-менеджера оценил покрытие жизненного цикла личного
проекта скиллами и агентами av-dev-pm. Скоуп сужен по ходу разбора: деплой и
разбор инцидентов делаются вручную, скиллов под них не заводим — три находки
из восьми сняты этим сразу.

Шаг 2 сессии требовал чисел, которых процесс отказался собирать решением.
cadence.md делал обязанностью пересмотр «ориентира по размеру спринта, прироста
беклога на закрытую задачу, времени на задачу» и «сколько заняли задачи против
ожидания». Данных нет: у записи нет дат заведения, взятия и закрытия, close
удаляет файл, sprint close очищает SPRINT.md. Хуже, «против ожидания» и «время
на задачу» требуют оценки и тайм-бокса, а session/SKILL.md в «Почему не Scrum»
их прямо не берёт — пункт противоречил решению через файл от себя. Числа не
пересматривались ни разу, поэтому выкинуты, а не подперты учётом дат. Осталось
качественное; рядом записано, что замеров нет намеренно, иначе следующий
читатель заведёт их обратно как недостающие. Шаг 3 пункт 9 переименован из
«переоценки по измеренному» в «по пройденному».

doc-consistency переехал с каждого синка на сессию, к doc-code-drift. Агент на
opus звался шагом 9 пайплайна, то есть 5-8 opus-проходов за спринт по документам,
меняющимся на несколько абзацев. Довод сильнее денег: расхождение между двумя
документами по определению требует двух, а на большинстве задач синк правит
один. И пачка, отбираемая работой, не видит того, чего работа не касалась, —
а расхождение живёт ровно там. Это снимает открытый вопрос REMAINING про охват
парного статуса ADR. Цена — потеря привязки находки к задаче, принято сознательно.

Отмена цели получила порядок, но не флаг. close запрещал закрыть цель с живыми
задачами и не говорил, что с ними делать. Теперь: сперва задачи поштучно
(close --reason своей причиной либо edit --goal на другую), потом цель в
REJECTED.md, а не в Готово. Флаг --cascade отвергнут: поштучный разбор — не
церемония, а единственный момент, когда видно, что переживёт цель. Место
процедуры — переоценка на сессии, отмена цели и есть разбор её задач.

У брошенного спринта появился второй законный исход. --dissolve везде был
привязан к блокеру, и вернувшийся к месячному набору не имел законного хода:
двигать нельзя, распускать не по чему. Теперь роспуск объясняется блокером или
тем, что набор протух. Порога в неделях нет — тот же класс, что выкинутые
числа: счётчик простоя пришлось бы вести руками. Признак не срок, а что набор
перестал быть твоим. Плюс точка входа «вернулся, а спринт открыт» и триггер в
description скилла.

Журнал канона прогоняется как есть, схлопывать 3 и 4 не стали. Взамен появилась
проверка исхода: шагом 6 adopt и шагом 6 upgrade зовутся оба судьи документов.
Это ответ на открытый вопрос «как проверять, что канон не разошёлся с проектами
после upgrade»: check сверяет число в .pm.json с версией скрипта и про существо
записи не знает ничего, а записи применяются руками.

Износ обязательных «границ покрытия» не правится: это гипотеза, а не находка.
Записана наблюдением к первой обкатке. Предложение агента поднять обкатку выше
калибровки снято — TODO уже так устроен, агент спутал «главный риск» с «первое
в очереди»; в REMAINING добавлена оговорка против того же прочтения.

Тема 31 в DECISIONS.md, следствия 117-123.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 14:02:04 +03:00
avandClaude Opus 5 c3e0a6d01f удалён av-dev-backlog: заморозка стоила дороже, чем удаление
Плагин был помечен устаревшим решением Q и жил до перевода jellybit.
Удалён раньше этого срока: условие пережило свою причину.

Заморозка выглядела бесплатной, а платила собой в каждой проверке
репозитория — exclude в pyproject.toml, SKIP_DIRS в copies.py, два абзаца
README, оговорка в описании маркетплейса, чтобы не ловить триггер
«добавь задачу в беклог». Пять исключений ради 706 строк, которые никто
не читает, и каждое надо объяснять всякий раз, когда спрашивают, почему
проверка обходит каталог.

Причина условия отпала раньше названного срока. docs/backlog/ читает не
backlog.py, а av-dev-pm:tasks — adopt.md и адаптер в tasks.py держат ту
же раскладку как вход миграции. Плагин перестал быть единственным, кто
её знает, ещё когда писался adopt, и «живёт до перевода последнего
проекта» с тех пор охраняло пустоту. Перевод jellybit на канон это не
задевает.

Порядок вышел обратный ожидаемому: плагин удалён из маркетплейса, а с
jellybit снят после. Ожидалась ручная чистка enabledPlugins и
installed_plugins.json, но claude plugin uninstall отработал штатно — он
идёт по реестру, а не по манифесту маркетплейса. Раздел «Снятие» в README
переписан с частного случая на общую процедуру, предупреждение заменено
проверенным фактом.

Записи Q и HH получили парные статусы, тема 30 в DECISIONS несёт причины
и три следствия, пункт 5 TODO отмечен, открытый вопрос из REMAINING убран.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 12:32:29 +03:00
avandClaude Opus 5 52cc4d05d4 разбор находок doc-consistency: остатки модели типов и копии в ссылки
Первый прогон агента — по репозиторию, который его же и содержит. Два
прохода, 17 находок, все подтверждены по файлам.

Пять находок — остатки прежней модели типов в файлах, до которых я не
дошёл двумя коммитами раньше. adopt.md держал имена секций роадмапа
канона 2 («порядка», «темы») и «пустой goal законен только у идеи»;
from-review.md и TODO.md — упразднённый [idea]; task-batch в другом
плагине — «задачи-идеи». Правку модели я вёл от документов, которые
менял, а не от тех, что на них ссылаются: grep по упразднённому слову
дал бы все пять за минуту.

Самая дорогая находка оказалась моей и свежей. Таблица типов в canon.md
объявляла цель у fix запрещённой, а tasks/SKILL.md и task-fix.md —
необязательной; код на стороне вторых. Копия разошлась с домом за один
день, обе половины писал один проход. Поправлено не значение, а причина:
canon.md дважды объявлял, что фиксирует только словарь типов, — значит
колонкам «разделы» и «цель» в нём не место. Осталась таблица из двух
колонок и ссылка на дом схемы.

Перечень «чем держат проект» пересказывался втроём и разъехался:
«метрики и логи» против «мониторинга», «проверки» есть в двух из трёх.
При этом tasks/SKILL.md ссылался на дом рядом с собственным пересказом —
ссылка не мешает копии разойтись, если копия всё равно стоит. Перечень
остался в canon.md, два места ссылаются.

README пересказывал раскладку канона блоком кода, и копия была уже
неполна — не хватало путей, чьё отсутствие docs.py считает нарушением.
Заменено ссылкой. Там же измеренное число из DECISIONS III заменено
ссылкой на решение.

REMAINING дублировал два отмеченных сделанными пункта TODO и держал
счётчики, которые обязан двигать человек: «двенадцати тем и 16 коммитов»
(стало 28 и 52), «три неизмеренных изменения» (стало больше). Счётчики
отменены как класс, причина записана в шапку. Открытый вопрос про парный
статус ADR переформулирован: судья появился, открыт остался охват.

Три противоречия вне av-dev-pm: --roadmap-sections перечислен среди
флагов init прозой того же файла, объявляющей, что его нет; review-ops
берёт журнал docs/review.md и тут же объявляет историю инцидентов
принципиально недоступной; «честный предел» конвейера отменял целиком
документ docs/research/. Плюс битый якорь ссылки на раздел вычитки.

Находка про Co-Authored-By снята как неверная: агент прочитал
av-dev-git/skills/commit/SKILL.md как описание практики этого
репозитория, а это продукт, уезжающий в чужие проекты. Устав агента не
различает «документ про нас» и «документ про то, что мы производим» —
остаток записан в REMAINING, в устав пока не дописан.

DECISIONS тема 29 (ЧЧШШ–ЮЮЯЯ, следствия 109–113).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 11:21:40 +03:00
avandClaude Opus 5 354a6b03d5 канон 4: слаг подкреплён проверкой, обещанный судья заведён
Оба пункта заметок оказались одним классом: правило записано и никем не
исполняется.

Слаги. canon.md говорил «слаги файлов, capability и задач — английские,
kebab-case» одной строкой в хвосте раскладки, а docs.py имён файлов не
смотрел вовсе. Итог нашёлся в самом плагине: единственный пример ADR в
скилле docs назывался ADR-2026-08-03-ochered-tablicej. Раскладка канона
при этом приглашала к нарушению — в схеме стояли плейсхолдеры <тема>.md,
то есть слово «тема» по-русски там, где надо писать <slug>.

docs.py check теперь смотрит имена: кириллица и не-kebab-case жёстко,
форма ADR-ГГГГ-ММ-ДД-slug.md жёстко, транслит эвристикой, то есть
замечанием. Проверяются docs/conventions, docs/research, docs/adr и имена
capability; каталог задач не трогается — его слаги ведёт tasks.py.

Набор маркеров транслита подобран так, чтобы ложных срабатываний не было
вовсе: выброшены ost (ловит post, cost), sch (schema), ya (yaml), nost
(nostalgia), хвост ii (radii). Цена названа в комментарии —
sostoyanie-partii проходит мимо. Правило, краснеющее на правде, приучает
пролистывать весь блок, и это дороже пропуска.

Агенты. В canon.md есть таблица «Что проверяет машина, а что человек», и
её правая колонка — смысловой дубль, поведение в architecture.md,
протухший факт, достаточность честной строки — три версии описывала
работу, которую никто не делал: скилл canon предлагал агенту судить об
этом самому, то есть проверять то, что он же и писал.

Заведены двое, разрез по глубине — тот же довод, что развёл task-form и
doc-wording. doc-consistency читает docs/ и openspec/, сверяет документы
между собой (факт в двух домах, прямое противоречие, поведение в обзоре
вместо спек, ADR без ссылки на design.md и без парного статуса, число без
провенанса, заглушка вместо честной строки) и зовётся на шаге синка
документации. doc-code-drift читает репозиторий, отвечает на «этот факт
ещё верен» и зовётся раз в спринт на сессии.

Перечень фактов, сверяемых с кодом, закрыт: имя ветки, команды, пути,
зависимости поимённо, настройки с числом, единые точки проекта,
capability, проверяемые инварианты. «Сверить архитектуру с кодом» —
задача без дна, и агент, которому её поставили, выдаёт правдоподобную
труху. Отсюда форма его доклада: начинается таблицей проверенного, а не
находками, — по ней видно, чего он не смотрел.

Карта домов уехала в устав doc-consistency помеченной копией: устав
ссылался на файл плагина, а агент работает в репозитории проекта, где
плагина может не быть. copies.py её сторожит.

Попутно: докстрока copies.py показывала закрывающие маркеры как
<!-- /дом -->, а код требует <!-- /дом: <id> -->. Нашлось первой же
попыткой ими воспользоваться.

DECISIONS тема 28 (ННОО–ХХЦЦ, следствия 105–108).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 10:20:39 +03:00
avandClaude Opus 5 228b6c7eee канон 4: тип записи стал единственной осью и задаёт схему
Осей было две — тип записи (goal/idea/task) и род работы (kind:<род>
тегом), — и ортогональность у них была фальшивой: из двенадцати клеток
произведения законны шесть. У цели род запрещён, у задачи обязателен, у
идеи пуст и на практике не ставится. Плюс «алгоритм работы над записью
такого типа» крепится не к task, а к fix и research, то есть к роду:
ось, к которой пишется алгоритм, и была настоящим типом. Схлопнуто в
одну ось из пяти значений: goal | feature | fix | chore | research.

Тип idea упразднён отдельно и по другой причине: он значил не род
работы, а незаполненность, а состояние типом быть не может — оно
меняется по мере того, как запись дописывают, а тип меняют командой.
Теперь состояние выводится из заполненности: research без раздела
«Вопрос» это сырьё. В спринт не берётся, как и прежняя идея, лежит в
конце категории, отбирается list --raw.

Дом типа — поле меты «Тип» первой строкой, эмодзи в H1 производна.
Прежнее «отдельного поля типа нет: два места для одного факта
разъезжаются» отменено собственным аргументом: он был против префикса
плюс поля, а при переносе дома место остаётся одно. Эмодзи стоит в H1,
а не в строке индекса, чтобы инвариант «заголовок в индексе дословно»
остался нетронутым.

Поле места названо по типу: «Секция» у цели (часть роадмапа, состояние
очереди), «Категория» у задачи (полка домена, куда её вернёт sprint
drop). Одинаковое переименование закрепило бы конфляцию; какое поле
обязательно, решает тип — то самое, ради чего затевалась правка.

Два новых обязательных раздела выросли из правил, которые были записаны
и которые нечем было проверить. «Не воспроизводится — это research, а не
fix» стояло в каноне: теперь есть раздел «Воспроизведение». Приёмка
разведки — «записанный ответ, а не изменённый код» — тоже стояла, но
sprint take требовал от research два-пять критериев с оракулами, и они
писались ради проверки; вместо них «Вопрос» и «Куда ляжет ответ».

Сортировка «по важности» из заметок не взята: она требует, чтобы кто-то
важность поддерживал, а это приоритет, от которого отказалось правило 4.
Взято только «сырьё в конец категории» — этот порядок выводится из типа
и заполненности, а не назначается человеком, и потому проверяется
машиной.

TYPE_SCHEMA кормит и body_template, и schema_verdict: иначе add кладёт
то, на чём sprint take потом откажет. check --fix мигрирует за один
проход — kind:/[goal]/[idea] в поле «Тип», эмодзи в заголовок, «Секция»
→ «Категория», сырьё в конец. Тип, которого неоткуда взять, не
угадывается: feature от chore машина не отличает, такие записи уходят в
НЕОДНОЗНАЧНО поимённо.

Попутно закрыт класс отказов в --fix: шагов, правящих мету, стало пять,
и второй, перечитавший файл с диска, стирал правку первого. Общий
stage() поверх отложенных правок; до этого корректность держалась на том,
что шагов было мало.

Устав на тип отдельным файлом — references/task-<тип>.md, пять штук:
схема, алгоритм, что видит машина и что человек. Агент task-form получил
правило «тип сходится с тем, что в записи написано» с проверяемыми
расхождениями.

Обкатано на демо-наборе из 13 записей: миграция за один проход, второй
прогон даёт ноль починок; fix без «Воспроизведения» и сырьё в спринт не
идут, годная feature берётся.

DECISIONS тема 27 (ААББ–ЛЛММ, следствия 101–104).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 10:00:05 +03:00
avandClaude Opus 5 069205ac69 канон 4: секция «Сопровождение», «Готово» вниз, порядок закреплён
healthlog уже переехал на канон 3, а переименование секции я внёс
правкой записи версии 3 задним числом — то есть переписал текст, по
которому он ехал. Посылка «ни один проект на каноне 3 не стоит» была
ложной, решение ШШШ отменено.

Запись версии — черновик ровно до первого переехавшего проекта. После
этого она история, и любое изменение канона заводит новую версию, даже
если меняется одно слово. Проверять дёшево: grep '"canon"' по живым
проектам. Дорого обратное — проект, повышенный по тексту, которого
больше не существует, невоспроизводим.

Запись версии 3 восстановлена дословно (Разработка | Tooling),
переименование уехало в версию 4. jellybit, стоящий на каноне 2,
прочтёт обе записи подряд и заведёт Разработка, чтобы через шаг
переименовать; в шаг версии 3 добавлена оговорка «едешь сразу на 4 —
заводи Готово последней и не переставляй дважды».

«Готово» переехало вниз, и порядок секций стал каноническим.
Достигнутое копится: через год этой секции больше, чем всех остальных
вместе, и стоя первой она отодвигает за экран то, ради чего роадмап
открывают чаще всего. Порядок проверяет roadmap_lint, переставляет
check --fix — вместе с содержимым секций, потому что двигать десяток
строк руками это работа, на которой ошибаются. Чужую секцию
перестановка не трогает вовсе: её место в порядке неизвестно.

Индексы позиций считаются из самого кортежа: ACHIEVED был 0 и стал 3,
хардкод пережил бы перестановку молча и сломал бы close.

Обкатка нашла два дефекта оформления, оба порождённые самой
перестановкой. Отбивка нужна и перед заголовком — сдвиг блоков ставит
два заголовка вплотную. Удаление строки индекса оставляет две пустые
подряд, и пустоты копятся. Проверка оформления теперь сверяется с самим
нормализатором, а не своим набором условий: два описания одного правила
разъедутся, и check начнёт молчать о том, что --fix правит.

CANON_VERSION = 4 в docs.py, примеры .pm.json в canon.md и skeletons.md.

DECISIONS тема 26 (ЭЭЭ, ЮЮЮ, ЯЯЯ, следствия 98–100).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 20:29:15 +03:00
avandClaude Opus 5 d7e9740c73 секция роадмапа «Сопровождение» и общий словарь трёх мест
«Разработка» называла слишком много: роадмап весь про разработку, и
секция с таким именем не отличалась от остальных ничем. Стало
Сопровождение | Operations.

Смысл расширен вместе с именем: было «инструмент и процесс», стало «чем
держат проект: инструмент, процесс, эксплуатация». Расширение не
косметическое — английское Operations при узком смысле обещало бы
эксплуатацию, а внутри лежал бы линтер. Метрики, логи, инфраструктура и
выкладка в эту секцию просятся и так.

Заодно синхронизирован словарь трёх мест канона, которые про одну тему.
Сопровождение — всё, чем держат проект; эксплуатация — его часть, работа
системы на проде. ROADMAP.md, секция Сопровождение — план работ;
architecture.md, раздел «Эксплуатация» — как устроено сейчас;
эксплуатационный проход ревью — оптика проверки. Сливать их в одно слово
было бы ошибкой: они отвечают на разные вопросы. Синхронизирован
словарь, а не границы; дом — canon.md. Слово «поддержка» запрещено
вовсе: в нём слышится помощь пользователю.

Граница с возможностями проходит по тому, кто наблюдает: «приложение
сообщает о своём состоянии» — возможность, «дежурный видит состояние на
одном экране» — сопровождение.

Версия канона не менялась, и это законно: ни один проект на каноне 3 не
стоит, оба держат канон 2. Запись версии 3 правится как черновик, а не
как история — версия отделяет одно состояние проектов от другого, а не
одну редакцию текста от другой.

DECISIONS тема 25 (ЧЧЧ, ШШШ, ЩЩЩ, следствия 96–97).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 20:17:53 +03:00
185 changed files with 23313 additions and 12650 deletions
+3 -13
View File
@@ -6,24 +6,14 @@
},
"plugins": [
{
"name": "av-dev-pm",
"source": "./av-dev-pm",
"description": "Управление продуктом: канон документов проекта, задачи и цели вместо приоритетов, спринт под одну цель с заморозкой набора, старт проекта интервью по брифу и приведение существующего к канону. Ничего не выполняет сам и никакого пайплайна не требует: задача выполняется чем угодно, а канон описывает документы, из которых конвейер ревью берёт проектную конкретику."
},
{
"name": "av-dev-pipeline",
"source": "./av-dev-pipeline",
"description": "Проведение задачи через цикл SDD и конвейер ревью с обязательным триажем, плюс прогон нескольких задач разом. Требует OpenSpec. Задача принимается и обычным текстом; плагин av-dev-pm опционален — он даёт документы канона для проходов ревью и учёт задач, без него прогон деградирует поразрядно и говорит об этом."
"name": "av-dev",
"source": "./av-dev",
"description": "Личный процесс разработки одним плагином: форма раскладки, документы проекта, учёт работ и работа по задачам. Форму держит скилл canon — три операции одной машиной сравнения (check, adopt, upgrade) со скриптом docs.py и общим журналом версий, по которому повышается вся раскладка, включая каталог задач; имя без префикса, потому что форма общая у всех частей. Документы — канон раскладки (CLAUDE.md плюс docs/: паспорт, архитектура, схема БД, безопасность, конвенции, разведка, ADR, журнал ревью), заведение нового проекта интервью в doc-init, ведение содержимого по ходу разработки в doc-sync, суд о смысловом здоровье в doc-healthcheck двумя агентами разом. Учёт — задачи каталогом markdown-файлов в task-track: у задачи тип (feature, fix, chore, research), задающий её схему, а у проекта — стадия (build — беклог это план стройки от базы к деталям, порядок строк значит зависимость; support — очередь правок, порядок значит важность); очередь расставляет интерактивный task-groom порядком строк. Работа — одна задача от постановки до закрытия скиллом code-resolve: точка входа одна, сценария три (решение циклом SDD с чекпоинтом, разведка без кода, обслуживание без change), и выбирает сценарий сам скилл, прочитав постановку; плюс конвейер ревью code-review с детерминированным гейтом, сверкой со спеками и обязательным триажем, плюс code-deep-review — глубокое ревью области кода тяжёлыми проходами, которое зовут время от времени, а не на задаче, плюс code-openspec, который заводит и проверяет openspec/. Версия раскладки и настройки живут в .av-dev.toml в корне репозитория. Части включаются следом в проекте, а не установкой: нет docs/ — документы не ведутся, нет каталога задач — учёт остаётся владельцу, нет openspec/ — его заводит сценарий решения. Коммиты — отдельный плагин av-dev-git."
},
{
"name": "av-dev-git",
"source": "./av-dev-git",
"description": "Git-обвязка для личных проектов. Скилл commit — сообщения коммитов в личном стиле (русский, scope-префикс, средняя детализация, без co-authored)."
},
{
"name": "av-dev-backlog",
"source": "./av-dev-backlog",
"description": "УСТАРЕЛ, заменён плагином av-dev-pm. Старый формат беклога: один каталог задач с индексом README и приоритетами секциями. Оставлен до перевода последнего проекта; новые проекты не подключают."
}
]
}
+3
View File
@@ -2,3 +2,6 @@
__pycache__/
.venv/
.ruff_cache/
tmp/
/NOTES.md
-1730
View File
File diff suppressed because it is too large Load Diff
-85
View File
@@ -1,85 +0,0 @@
# Как процесс дошёл до текущей формы
Сжатие черновика `AGENTIC-TASKS.md` (497 строк), лежавшего незакоммиченным в
корне healthlog. Правила процесса из него переехали в плагины и здесь **не
повторяются** — второй дом для тех же правил ровно то, против чего документ и
был написан. Остаётся то, чего в плагинах нет и быть не должно: **что отвергнуто
и почему, и числа первого замера**.
Решения текущего круга разбора — [DECISIONS.md](DECISIONS.md).
## Что отвергнуто и почему
### Scrum целиком
Терминология близка — спринт, груминг, определение готовности, ретроспектива, —
и она удобна: не нужно изобретать слова. Но добрая половина Scrum существует ради
синхронизации людей, которых здесь нет: исполнителей двое, человек и агент.
**Не взято:** тайм-бокс (спринт ограничен объёмом, а не временем), velocity и
оценки в очках, ежедневный стендап (стендап — это и есть диалог), планирование
отдельно от груминга (владелец беклога один), роль скрам-мастера.
**Взято:** цель спринта, заморозка набора, определение готовности, груминг —
каждое потому, что снимает решение, которое иначе принимается заново каждый раз.
**Ретроспектива взята содержанием, но не отдельным ритуалом**: она шаг той же
сессии. Отдельная встреча ради трёх вопросов — плата ритуалом без выгоды.
### Приоритеты у задач
Заменены целью. Ни секциями, ни списком: «что делать дальше» отвечает набор
спринта, а между спринтами порядок не нужен никому — брать задачи вне спринта
запрещает заморозка. Отсюда нет ни «повысить», ни «встать раньше»: вместо
повышения — смена цели или включение в набор.
### Секция «блокеры» в беклоге
Блокер — **состояние** (спринт не может продолжаться ни одной задачей), а не
полка: он живёт ровно до ответа человека, и записи в такой секции не успевают
жить. Основание измерено: **два «блокера» из двух ничего не блокировали** — в
обоих файлах записано «что заблокировано: ничего». Отсюда разделение вопроса и
блокера.
### Запись о сделанной задаче
У сделанной задачи записи не остаётся: файл и строка удаляются. Ей хватает
коммита и документации; вторая запись была бы вторым домом для того же факта.
Вопрос «что было в спринте N» отвечается даром — `SPRINT.md` лежит под git.
## Числа первого замера
Одна сессия, шесть закрытых задач. **Выборка нетипичная, статус — первый
замер.** Приведены не как константы, а чтобы следующий замер было с чем
сравнить.
- **Беклог вырос с 29 до 38**: заведено 15, закрыто 6 (две родились и умерли
внутри сессии). Прирост **2,5 задачи на одну закрытую** — ревью и
эксплуатационные проходы производят работу быстрее, чем мы её потребляем.
- **Одна из шести задач была внеплановой** — дозакрытие находок, вставленное в
ход работы, потому что дефект затирал маршрут тренировки необратимо, а
пересборка журнала повторяла то же поражение. Отсюда класс «необратимый
ущерб» как единственное, что врывается в замороженный спринт: правило не
придумано, оно уже применялось.
- **15 часов на шесть задач**: пять заняли от 1 ч 16 мин до 2 ч 14 мин (медиана
≈ 1 ч 55 мин), шестая — 5 ч 42 мин в два захода. Мерилось **до** сужения
конвейера ревью; замер устарел и подлежит повторению.
- **Шесть задач за сессию** — предел одного контекста, а не спринта. Спринт
сессией не ограничен, перенос числа условен.
Умолчание «5–8 задач в спринте» выведено отсюда и остаётся **ориентиром, а не
законом**. Пересматривается на разборе прошедшего спринта — шаг 2 сессии, и ничей
другой.
## Что из черновика было не решено и решено позже
| Вопрос черновика | Где решён |
| --- | --- |
| название процесса | решение Z: имени нет, процесс это `av-dev` |
| «Ближайшая цель» прозой в `docs/plan.md` как второй дом цели спринта | решение E: `plan.md` растворяется в `PLAN.md` целей |
## Судьба самого черновика
Документ описывал процесс, а процесс живёт в плагинах этого репозитория, не в
healthlog. Содержимое разошлось: правила — в `av-dev-pm:tasks` и
`av-dev-pm:session`, обоснования и числа — сюда. Оригинал в git не коммитился и
удаляется при переезде healthlog на канон.
+392 -104
View File
@@ -3,55 +3,172 @@
Личный маркетплейс плагинов для разработки. Процесс имени не имеет — он и есть
`av-dev`.
Что решено и почему — [DECISIONS.md](DECISIONS.md). Что осталось сделать —
[TODO.md](TODO.md) и [REMAINING.md](REMAINING.md). Как процесс дошёл до текущей
формы — [HISTORY.md](HISTORY.md).
Что решено и почему — [журнал решений](decisions/README.md).
## Плагины
- **av-dev-pm** — управление продуктом. Владеет всем `docs/`.
- `init` — новый проект: интервью по свободному описанию замысла → первичная
документация;
- `canon` — привести проект к канону документов: `check` / `adopt` /
`upgrade`, плюс скрипт `docs.py`. Там же живёт язык проектных текстов —
информационный стиль, англицизмы, жаргон;
- `docs` — содержимое канона по ходу разработки: ADR из архивного
`design.md`, промоут конвенций, запись в разведку и журнал ревью, чистка
архитектуры;
- `tasks` — задачи и цели каталогом markdown-файлов; вычитывают их два
отдельных прохода: `task-form` (форма записи) и `doc-wording` (язык);
- `session` — ритуал между спринтами и ведение спринта.
- **av-dev-pipeline** — исполнение. **Требует OpenSpec.**
- `task-pipeline` — задача через полный цикл SDD, от постановки до коммита;
- `task-batch` — несколько задач разом, каждая в своём worktree;
- `review-pipeline` — конвейер ревью: гейт, сверка со спеками, враждебные
постановки, эксплуатационный постмортем, независимая реализация,
архитектура, обязательный триаж. Девять агентов-проходов, четыре ступени
стоимости: `quick`, `standard`, `wide`, `deep`.
- **av-dev-git** — `commit`: сообщения в личном стиле.
- **av-dev-backlog** — **устарел**, заменён `av-dev-pm`. Живёт до перевода
последнего проекта; как снять с проекта — [Снятие](#снятие).
Плагина два: **av-dev**весь процесс, и **av-dev-git** — сообщения коммитов.
Второй работает в любом репозитории, в том числе не приведённом к канону. До 13 августа 2026 процесс жил тремя плагинами
(`av-dev-docs`, `av-dev-tasks`, `av-dev-code`); раскол делался под раздельную
установку, она не понадобилась ни разу, и плагины слились —
[тема 64](decisions/64-three-plugins-merged.md) журнала решений.
Соглашение об именах: имя **плагина** длинное с префиксом `av-dev-`, имена
**скилов** внутри — короткие. Вызов выходит вида `/av-dev-<плагин>:<скилл>`.
Имя **скилла** несёт префикс материала, с которым он работает: `doc-`, `task-`,
`code-`. Вызов выходит вида `/av-dev:<скилл>`. Префикса нет ровно у одного —
`canon`: он работает не с материалом, а с **формой**, общей у всех частей
проекта.
Кто кого зовёт (стрелка — вызов через пространство имён, не импорт):
### av-dev — форма, документы, учёт, работа
**Форма раскладки.** Одна на весь проект, и держит её один скилл.
- `canon` — раскладка проекта и её обновление: `check` / `adopt` / `upgrade`,
плюс скрипт `docs.py`. `check` сверяет раскладку документов, `adopt` заводит
все части сразу и зовёт владельцев каталога задач и `openspec/`, `upgrade`
повышает **всю** раскладку по журналу версий — общему, и на документы, и на
каталог задач. Содержимого он не ведёт: это соседние скиллы.
**Документы проекта.** Владеют **содержимым** `docs/` и `CLAUDE.md`; раскладка —
у `canon`.
- `doc-init` — новый проект: интервью по свободному описанию замысла →
первичная документация;
- `doc-healthcheck` — здоровье документации **судом, а не машиной**: не
разошлись ли документы между собой и с кодом. Зовёт двух агентов на весь канон
разом — `doc-consistency` (документы между собой и с openspec) и
`doc-code-drift` (факты против кода) — и разбирает урожай порциями. Дорого,
поэтому не на каждой задаче. Язык документов вычитывает отдельный агент
`doc-wording`, и зовут его не отсюда, а те, кто только что писал текст:
`doc-sync`, `doc-init` и `canon`;
- `doc-sync` — содержимое канона по ходу разработки: ADR из архивного
`design.md`, промоут конвенций, запись в разведку и журнал ревью, чистка
архитектуры. Правки двух родов, и спрашивается один: отражение сделанного
пишется молча, новая запись и новая норма — только по слову человека. Он же
считает и говорит строкой, сколько задач сделано с прошлой сверки документов.
**Учёт работ.** Владеет каталогом задач.
- `task-track` — задачи каталогом markdown-файлов: у каждой тип (`feature`,
`fix`, `chore`, `research`), задающий её схему, а у проекта — стадия (`build`
или `support`), решающая, что значит порядок строк беклога;
вычитывают их два отдельных прохода: `task-form` (форма записи) и
`task-wording` (язык записей);
- `task-groom` — груминг беклога: что сейчас самое важное и что перестало быть
важным. Ответ записывается **порядком строк** — приоритет это свойство
очереди, и живёт он в индексе. Интерактивный: разбирает вопросы пачкой,
переоценивает порциями по 5–8, расставляет верх очереди с доводом на каждое
движение.
**Работа по задачам.** Владеет `openspec/`. **Сценарий решения требует OpenSpec
и заводит его сам** — разведке и обслуживанию он не нужен.
- `code-openspec` — завести, настроить и **проверить** `openspec/` в проекте:
`openspec init`, замена примера в `config.yaml` настройкой канонической формы,
скрипт `openspec.py` (форма файла + сверка слепка с живой версией
инструмента). Каталог принадлежит конвейеру, а не канону: без конвейера он
проекту не нужен, и `docs.py` о нём молчит;
- `code-resolve` — одна задача от постановки до закрытия. **Точка входа одна, а
сценария три, и выбирает сценарий сам скилл, прочитав постановку:**
классифицировать задачу до вызова человек всё равно не может — «есть ли
очевидный способ решения» и «меняется ли спека» видно после чтения записи.
**Форм постановки две, и обе полноправны:** запись каталога и просто текст,
переданный вызовом, — так же берёт постановку `opsx:propose`. Текстом идут все
три сценария; отпадают ровно те шаги, у которых пропал предмет: `ready` гонять
нечего, закрывать нечего, а тип, границы и понимание постановки называются
вслух первой репликой — человек, написавший текст, рядом и правит одной фразой.
Записи в каталог скилл при этом не заводит ни до работы, ни задним числом.
**Решение** идёт циклом SDD с чекпоинтом сразу после предложения: объяснение
человеческим языком, повод скорректировать ход до того, как написан код.
**Обслуживание** (тип `chore`: тулчейн и сборка, зависимости, гит-хуки,
перенос, чистка) change не заводит и планового стопа не имеет вовсе:
дельта-спек у него нет **по построению**, то есть цикл SDD здесь не урезан, а
остаётся без входа. Ревью идёт фиксированным планом без change —
`autotests` и `operations`, плюс `conventions` с техническим
разбором, если дифф трогает код; главный шаг сценария — синк документации,
потому что обслуживание чаще прочих двигает как раз те факты, которые
сверяются с кодом. Правка гейта сверяется по составу проверок, а не по цвету.
Нашлась дельта-спека — задача **оказалась шире своего типа**: работа
останавливается, тип называется (`fix` или `feature`), человек получает
объяснение простым языком и два решения — переформулировать запись и решать её
процессом того типа следующим прогоном либо прекратить; «доделать как
обслуживание» решением не является.
**Разведка** (тип `research`, сырая идея, мутная постановка) кода не пишет
вовсе: её чекпоинт — варианты, 2–4 способа решить с ценой каждого, — стоит до
первого написанного требования, а исход уезжает в документы канона и в задачи.
Обе пачки — документы и записи — **вычитываются перед коммитом** своими
проходами: `doc-wording` по документам, `task-form` и `task-wording` по
записям. Выбранный способ реализуется **следующим прогоном**, и запускает его
человек: смена сценария по ходу — событие с названным исходом, а не тихий
поворот.
**Письмо уходит агентам:** спеки, код и правки по находкам ревью пишет
отдельный агент по заданию, а оркестратор ставит задание и читает короткий
возврат. Контекст ему нужен под чекпоинт, сверку плана с исходом и доклад —
содержимое тронутых файлов и вывод гейта вытесняют оттуда постановку и
одобренное, и вытесняют молча. Разведка сюда не попадает: её записка и записи
задач и есть исход, из которого собирается доклад.
Все три сценария лежат справочниками и одинаково —
`references/solve.md`, `references/maintain.md` и `references/research.md`; в
самом скилле только вход, развилка и правила, не зависящие от сценария;
- `code-review` — конвейер ревью **по темам**: документ проекта либо заводит
тему проверки, либо питает чужую тему источником, либо процессный и в ревью не
читается вовсе. **Состав постоянный, метки у прогона нет:** гейт, сверка со
спекой, разбор кода, триаж; приёмник тем идёт, когда у проекта есть свои темы.
Цикл задачи проверяет **корректность и механику** против записанного критерия —
дельта-спеки, конвенции, инварианты `CLAUDE.md`, вывод инструментов; темы
`security`, `operations` и `architecture` закрыты в нём сверкой с записанными
инвариантами, и только. Находки по умолчанию чинятся инлайн и молча, человеку
уходит необратимое и то, что меняет дельта-спеки, а задачи из урожая заводятся
по его слову. Каждый проход — свой агент, перечень держит сам скилл;
- `code-deep-review`**глубокое ревью области**, а не задачи: модуля, слоя,
сервиса целиком. Здесь живут тяжёлые проходы, которых в цикле задачи нет, —
`review-adversary` строит путь и **прогоняет** падающий тест, `review-ops`
снимает числа замером, `architecture` судит форму решения на широком входе;
рядом идёт `code` по коду целиком. Исход — не правки, а разговор: находки
разбираются с человеком по одной, и согласованное уезжает задачами через
`task-track`. Дорого — не на
задаче и не по расписанию; вход копит сам цикл строками «отложено» в границах
покрытия.
### av-dev-git
`commit` — сообщения в личном стиле. Отдельным плагином потому, что нужен и в
репозитории, который к канону не приведён и никогда не будет.
Кто кого зовёт. Сплошная стрелка — вызов скилла через пространство имён, не
импорт; пунктир — совет позвать, а не вызов. Агенты-проходы в графе не показаны:
их зовут скиллы, названные выше.
```mermaid
flowchart TB
subgraph pipe["av-dev-pipeline — исполнение, требует OpenSpec"]
direction LR
batch["task-batch"] --> tp["task-pipeline"]
tp --> rp["review-pipeline<br/>9 агентов-проходов"]
batch --> rp
end
subgraph pm["av-dev-pm — управление продуктом, владеет docs/"]
direction LR
init["init"] --> tasks["tasks"]
canon["canon"] --> tasks
session["session"] --> tasks
docs["docs"]
subgraph avdev["av-dev — один плагин, весь процесс"]
subgraph pipe["работа по задачам; сценарий решения требует OpenSpec"]
direction LR
tp["code-resolve<br/>3 сценария: разведка,<br/>решение, обслуживание"] --> rp["code-review<br/>агенты-проходы"]
osp["code-openspec<br/>заводит и проверяет openspec/"]
deep["code-deep-review<br/>область, а не задача:<br/>тяжёлые проходы"]
end
canon["canon<br/>форма раскладки всего проекта"]
subgraph docsp["документы, владеют содержимым docs/"]
direction LR
init["doc-init"]
docs["doc-sync"]
hc["doc-healthcheck"]
end
subgraph tasksp["учёт работ"]
direction LR
groom["task-groom"] --> tasks["task-track"]
end
end
init --> tasks
init --> osp
canon --> tasks
canon --> osp
canon --> hc
hc --> tasks
docs --> rp
rp -.->|"строки «отложено»"| deep
deep --> tasks
groom -.-> hc
opsx["opsx:* — внешний плагин:<br/>explore, propose, apply, archive"]
git["av-dev-git: commit"]
@@ -61,41 +178,60 @@ flowchart TB
tp --> tasks
```
Зависимость **односторонняя: `av-dev-pipeline` знает про `av-dev-pm`, обратно —
нет.** Управление продуктом работает в проекте без конвейера; конвейер без
канона деградирует поразрядно и говорит об этом строкой.
**Скиллы зовут друг друга полным именем, а не по пути.** Внутри одного плагина
путь бы разрешился, но короткое имя разрешается в устаревшую проектную копию из
`.claude/skills/` — молча и без признаков подмены.
## Канон документов проекта
**Отсутствовать может не плагин, а часть раскладки проекта**: `.av-dev.toml`,
`docs/`, каталог задач, `openspec/`. Тогда вызывающий называет строкой, чего теперь не делает
никто, и работу не останавливает. Правило целиком —
[shared/absence.md](av-dev/shared/absence.md): оно нужно почти каждому скиллу, и
ни один им не владеет. Там же, в `shared/`, живут язык проектных текстов,
словарь сопровождения и **перечень осей процесса**
[axes.md](av-dev/shared/axes.md) — какие закрытые словари правят ходом работы,
где дом каждого и чего он **не** решает. Скиллы читают эти дома по ссылке, а
дословной копией оттуда уезжает лишь то, что обязано лежать внутри промпта.
## Канон раскладки проекта
Все проекты приводятся к одной раскладке — так проще ориентироваться, когда
проектов много, и рядом OpenSpec тоже держит строгую структуру. Определение —
[av-dev-pm/skills/canon/references/canon.md](av-dev-pm/skills/canon/references/canon.md).
проектов много, и рядом OpenSpec тоже держит строгую структуру. `CLAUDE.md` плюс
`docs/` (паспорт, архитектура, база, безопасность, конвенции, разведка, ADR,
ревью, задачи) и `openspec/` — **раскладка целиком, роли документов и правило
единственного дома живут одним домом**:
[canon.md](av-dev/skills/canon/references/canon.md). Здесь она не
пересказывается: копия перечня путей уже расходилась с домом, и как раз в
обязательных — в ней не хватало путей, чьё отсутствие `docs.py check` считает
нарушением.
```
CLAUDE.md инварианты с severity, команды, семантика гейта
docs/
.pm.json версия канона и пути для проверок
passport.md зачем и для кого; чем НЕ является
architecture.md как сложено — обзор; окружение и эксплуатация
database.md схема хранилища; настройки с числовым значением
security.md периметр; недоверенный вход; что вне модели
conventions/ как пишем код + что уже механизировано
research/ что показала реальность; числа с провенансом
adr/ почему — промоут поверх архивных design.md
review.md настройка конвейера + журнал дефектов
tasks/ роадмап (что умеет), беклог, спринт, отклонённое
openspec/
specs/<capability>/spec.md что система делает — нормативно
changes/archive/ архив изменений с design.md
```
**Документы канона делятся на три категории, и разрез проверяемый: можно ли по
документу сказать «в этом изменении сделано не так».** **Тема** — да, прямо
(`conventions`, `security`, `architecture` и любой свой документ проекта; список
тем открытый — завёл документ, завёл направление проверки). **Источник темы**
нет, но он задаёт границу для чужой темы (`passport`, `database`, `CLAUDE.md`,
`openspec/specs/`). **Процессный документ** — нет, он про то, как мы работаем
(`tasks/`, `review.*`, `adr.*`, `research.*`); ревью изменения по нему не судит.
Форма дома — файл или каталог, на выбор проекта.
**Отдельного файла-брифа для ревью нет.** Проходы читают эти документы напрямую;
карта «что нужно проходу → где лежит» —
[project-facts.md](av-dev-pipeline/skills/review-pipeline/references/project-facts.md).
Отдельного файла-брифа при этом нет — проходы читают документы напрямую; карта
«тема → её дом → что оттуда берётся» —
[project-facts.md](av-dev/skills/code-review/references/project-facts.md).
Прийти в старый проект и перевести его на канон — `/av-dev-pm:canon`. Канон
версионируется, и проекты повышаются по [журналу
версий](av-dev-pm/skills/canon/references/changelog.md).
Прийти в старый проект и перевести его на канон — `/av-dev:canon`.
Раскладка версионируется, и проекты повышаются по [журналу
версий](av-dev/skills/canon/references/changelog.md).
**Версия одна, и живёт она в `.av-dev.toml` в корне репозитория** — вместе с
настройками: `[docs] migrations` и секция `[tasks]`, которая говорит, где лежит
каталог задач и как названы его части. Версий было две, пока плагинов было три и проект мог
взять учёт работ без канона документов; теперь плагин один, и второе число
означало бы только вопрос, по какому журналу повышать. Прежние
`docs/.docs.json` и `<каталог задач>/.tasks.json` не читаются: увидев их,
`docs.py check` называет прежнюю раскладку и зовёт `upgrade` — запись 1
журнала. Формат TOML взят ради комментариев: файл лежит в репозитории проекта,
и назначение числа читают из него самого, а скрипты правят строку, а не
переписывают файл. Имя служебного файла по-прежнему называет владельца —
`.av-dev.toml`, `openspec/config.yaml`.
## Подключение
@@ -110,8 +246,7 @@ 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@av-dev-skills --scope project
claude plugin install av-dev-git@av-dev-skills --scope project
```
@@ -127,18 +262,28 @@ 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@av-dev-skills": true,
"av-dev-git@av-dev-skills": true
}
}
```
**При установке в проект, где лежали проектные копии** скиллов и агентов
(`.claude/skills/` — голые `task-pipeline`, `review-pipeline`, `task-batch`
и с префиксом проекта `<проект>-task-pipeline`,
`.claude/agents/<проект>-review-*.md`) — снеси их. Две копии одного скилла
расходятся, и побеждает та, что короче названа.
**При установке в проект, где лежали проектные копии** скиллов и агентов
снеси их. Перечень полный, и он же дом: скиллы носят его помеченной копией,
потому что предупреждают о том же в момент работы.
<!-- дом: проектные-копии -->
Голые имена в `.claude/skills/`: `resolve`, `review`, а у проектов прошлого
поколения ещё `task-pipeline`, `review-pipeline`, `task-batch`. С префиксом
проекта: `<проект>-task-pipeline`, `<проект>-review-pipeline`. Агенты:
`.claude/agents/<проект>-review-*.md`.
Две копии одного скилла расходятся, и побеждает та, что **короче названа**:
короткое имя разрешится в устаревшую проектную копию молча и без признаков
подмены.
<!-- /дом: проектные-копии -->
## Обновление
@@ -157,8 +302,7 @@ 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@av-dev-skills --scope project
claude plugin update av-dev-git@av-dev-skills --scope project
```
@@ -191,25 +335,25 @@ EOF
## Снятие
Действие, обратное подключению. Актуально для `av-dev-backlog`: плагин устарел,
и с каждого проекта снимается по мере перевода задач на канон `docs/tasks/`.
**Сначала перевод, потом снятие.** Задачи переводит `/av-dev-pm:canon`
(`docs/backlog/``docs/tasks/`). Снять плагин раньше — остаться со старой
раскладкой и без скилла, который её понимает.
Действие, обратное подключению.
```bash
cd /path/to/project
claude plugin uninstall av-dev-backlog@av-dev-skills --scope project
claude plugin uninstall <плагин>@av-dev-skills --scope project
```
Команда правит два места: убирает строку из `enabledPlugins` в
`.claude/settings.json` проекта и запись из реестра
`~/.claude/plugins/installed_plugins.json`. Снимок в
`~/.claude/plugins/cache/av-dev-skills/av-dev-backlog/<версия>/` не трогает — он
`~/.claude/plugins/cache/av-dev-skills/<плагин>/<версия>/` не трогает — он
общий для всех проектов. Маркетплейс тоже остаётся: `extraKnownMarketplaces`
нужен остальным плагинам.
**Удалять из маркетплейса можно и до снятия с проектов.** `uninstall` идёт по
реестру, а не по `marketplace.json`, и снимает плагин, записи о котором в
манифесте уже нет. Проверено на `av-dev-backlog`: удалён из маркетплейса,
снят с jellybit после — команда отработала штатно.
`--scope project` обязателен по той же причине, что и при установке: умолчание у
команды — user. `cd` в проект обязателен, но здесь ошибка слышна — вызванная не
оттуда, команда откажется словами `is not installed in project scope`, а не
@@ -230,10 +374,12 @@ claude plugin uninstall av-dev-backlog@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/ проверки репозитория: копии, диаграммы, фронтматтеры
av-dev/shared/ дома правил и общий читатель .av-dev.toml
scripts/ проверки репозитория и пересборка копий
pyproject.toml линтеры скриптов, только для этого репозитория
lefthook.yml гейт коммита: проверки документов
```
## Проверка скриптов
@@ -254,35 +400,38 @@ uv run pyrefly check # типы
линтеров, и любой сторонний импорт у него не разрешается. Список запретов —
не перечень мира, настоящий страж второй.
`av-dev-backlog` из проверки исключён намеренно: плагин помечен устаревшим и
живёт до перевода последнего проекта, после чего удаляется целиком. Правки в
замороженный код — риск без выгоды.
## Проверка фронтматтеров
## Проверка фронтматтеров и описаний плагинов
Фронтматтер читает не человек, а загрузчик: по `name` он разрешает вызов, по
`description` решает, звать ли скилл вообще. **Ошибка здесь не выглядит
ошибкой** — тем же способом, что и в диаграммах.
```
uv run python scripts/frontmatter.py # 0 в порядке, 1 расхождение, 3 не тот каталог
python3 scripts/frontmatter.py # 0 в порядке, 1 расхождение, 3 не тот каталог
```
Ловится три класса:
Ловится четыре класса:
- **двоеточие с пробелом в описании без кавычек.** Для YAML `: ` внутри
простого скаляра начинает вложенное отображение, так что «конвейер ревью: гейт,
сверка…» — это не текст с двоеточием, а синтаксическая ошибка. Так было
написано три описания из четырнадцати, и читались они правильно;
написано часть описаний плагинов, и читались они правильно — замер и разбор
в [решении Р61](decisions/17-notes-tier-work-kind-roadmap.md) журнала;
- **`name`, разошедшееся с именем каталога скилла или файла charter'а.** Вызов
разрешается по каталогу, а сообщение о промахе говорит «нет такого скилла»,
а не «имя не то»;
- **цвет charter'а, не отвечающий его модели.** Цвет кодирует модель, а не роль
прохода — раскладка живёт в
[review-pipeline/SKILL.md](av-dev-pipeline/skills/review-pipeline/SKILL.md),
[code-review/SKILL.md](av-dev/skills/code-review/SKILL.md),
разделе «Модель по проходу», здесь только её механизация. Держаться вниманием
правило не может: цвет ставится один раз при заведении charter'а, а модель
потом меняется калибровкой.
потом меняется калибровкой;
- **описание плагина, разошедшееся между манифестами.** У описания два дома:
`<плагин>/.claude-plugin/plugin.json` показывает его установленному плагину,
корневой `.claude-plugin/marketplace.json` — тому, кто выбирает, ставить ли.
Правят обычно один, и разойтись они успели уже трижды из четырёх. `copies.py`
этот класс не берёт: он смотрит markdown, а манифест — json. Отсюда и `*.json`
в глобе задачи гейта.
## Проверка копий правил
@@ -291,7 +440,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:
@@ -309,10 +458,90 @@ uv run python scripts/copies.py # 0 сошлось, 1 расхождение
сверку не входят. Маркер, уехавший в проект вместе со скелетом, там полезен: он
говорит, что у текста есть дом и правится он там.
**Дом правила, общего нескольким скиллам, лежит в `av-dev/shared/` и ни одному
из них не принадлежит.** Так живут язык проектных текстов, словарь
сопровождения и правило об отсутствующих частях раскладки: каждое нужно
многим, и хранить его внутри одного скилла значило бы отдать общее правило во
владение части.
**Копия при этом делается не всегда.** Пока плагинов было три, копия была
единственным способом: путь в дерево соседа не разрешался. Внутри одного дерева
справочник читается **по ссылке**, и дословная копия остаётся ровно там, где
текст обязан лежать **внутри самого промпта**: в уставе агента, где правило и
есть критерий суждения; в `SKILL.md`, который целиком и есть промпт скилла, — за
ссылкой скилл пошёл бы отдельным чтением, а правило нужно ему в тот момент,
когда он решает; и в скелетах, уезжающих в репозиторий проекта.
**Дом ставится в `shared/` только тогда, когда владельца нет.** У адресов
владелец есть: раскладку `docs/` держит `canon`, каталог задач —
`task-track`, и переносить их наружу значило бы отобрать у владельца его же
предмет. Общее без владельца живёт в `shared/`; чужое с владельцем остаётся
дома, а потребитель на него ссылается.
Скрипт ловит четыре вещи: копия разошлась с домом (с диффом), копия указывает не
на тот файл, дом остался без копий, разметка сломана. Чего он **не** ловит —
копию, которую забыли пометить: помечать — по-прежнему решение человека.
### Пересборка — `scripts/resync.py`
```
python3 scripts/resync.py # переписать тела всех разошедшихся копий из домов
# 0 готово, 2 разметка сломана, 3 не тот каталог
```
Правка дома касается стольких файлов, сколько у него копий, и последний из них
забывают — это и есть причина, по которой копии расходятся. Пересборка делает то
же машиной и потому дословна по построению.
**В гейт коммита скрипт не ставится, и это решение.** Автоматическая пересборка
протащила бы правку дома во все копии мимо глаз автора, а правка дома, чья копия
уезжает в репозиторий проекта, обязана ещё и попасть в журнал версий канона —
этого машина не напишет. Гейт поэтому только **называет** расхождение; согласие с
ним остаётся действием человека.
Разметку разбирает не он сам: `copies.py` импортируется целиком. Второй
разборщик той же разметки разошёлся бы с первым молча — ровно тот класс дефекта,
против которого механика копий и заведена.
**Ограда блока кода принадлежит месту, а не дому.** Одно и то же тело живёт в
доме внутри ```` ``` ````, а в скелете канона — внутри чужой, объемлющей ограды,
и своей там иметь не должно. Пересборка берёт тело дома без крайних оград и
надевает обратно ту, что была у копии; пустые строки по краям — так же.
## Проверка адресов документов
Адрес документа принадлежит одному плагину, а называют его все: `docs/*` стоит
примерно в сорока местах конвейера, `tasks/BACKLOG.md` — в нескольких местах канона.
Переименование в каноне до этих мест само не доходит.
```
python3 scripts/addresses.py # весь репозиторий
# 0 сошлось, 1 упразднённый адрес или опечатка, 3 перечень владельца недоступен
```
**Зачем машина, а не аккуратность.** Прогон ревью умеет честно деградировать:
дома темы нет — в границах покрытия появляется строка «документа в проекте нет»
с названной ценой. Протухший адрес попадает ровно в эту машинерию и выходит
**правдоподобным отчётом**, а не поломкой. Громкий признак ошибки деградацией
убран, и здесь он возвращается гейтом.
Перечень берётся из **константы владельца** — той, по которой он и так проверяет
раскладку (`docs.py`, `tasks.py`). Второй перечень прозой был бы вторым домом
ровно того сорта, против которого написан канон.
Судится **упразднённое, а не незнакомое**, и это следует из канона: список тем
открытый, всё, что проект кладёт в `docs/` сверх закрытых категорий, — законная
тема, и опровергнуть её нечем. Зато переименование ловится точно: канон, убирая
слот, кладёт его в карту переездов, и она здесь и есть перечень запрещённого.
Рядом единственная догадка — имя, **почти** совпавшее с каноническим: `securty`
это опечатка вероятнее, чем новая тема. Порог замерен по репозиторию: законные
имена дают до 0.64, опечатки — от 0.91.
Не проверяются журналы (они описывают прошлые состояния и задним числом не
переписываются), адреса `openspec/*` (раскладка чужого инструмента, владельца у
нас нет) и упоминания в комментариях скриптов — сверяется только markdown. Эти
границы скрипт печатает сам.
## Проверка диаграмм
Диаграммы `mermaid` живут исходником в markdown — картинок в репозитории нет.
@@ -320,15 +549,74 @@ uv run python scripts/copies.py # 0 сошлось, 1 расхождение
правдоподобно, диff показывает разумную строку, а рендер падает.
```
uv run python scripts/diagrams.py # 0 рендерятся, 1 нет, 3 нет mermaid-cli
python3 scripts/diagrams.py # весь репозиторий
python3 scripts/diagrams.py A.md B.md # только названные файлы
# 0 рендерятся, 1 нет, 3 нет mermaid-cli
```
Рендерит `mmdc` с PATH или `npx --yes @mermaid-js/mermaid-cli`; ни того ни
другого нет — код 3, а не молчаливый успех. Прогон занимает секунды на блок,
поэтому он не в гейте, а в руках того, кто правит диаграмму.
другого нет — код 3, а не молчаливый успех. Это самая дорогая проверка
репозитория: каждый блок — отдельный запуск mermaid-cli со своим chromium,
секунда с лишним. Поэтому у неё два рычага, и оба нужны гейту коммита: **блоки
собираются все сразу, а рендерятся параллельно** (пул потоков, порядок вывода
берётся из порядка сбора), и **проверять можно названные файлы, а не весь
репозиторий**. Весь репозиторий — три секунды вместо пятнадцати, один
файл — одна.
Чего проверка **не** ловит — расхождение диаграммы с прозой вокруг неё. Дословного
соответствия между текстом и графом нет, сличать нечего, и держится это
правилом старшинства, записанным рядом с каждой диаграммой: **в конвейере ревью
старший граф** (он и есть алгоритм планировщика, проза его объясняет), **в
остальных местах старшая проза** (диаграмма там сводка).
## Гейт коммита
Все семь проверок стоят в `pre-commit` через [lefthook](https://lefthook.dev) —
конфиг в [lefthook.yml](lefthook.yml), ставится один раз на клон:
```
lefthook install # пишет .git/hooks/pre-commit
lefthook run pre-commit # прогнать руками, не коммитя
```
| Проверка | Когда идёт | Что смотрит | Сколько |
| --- | --- | --- | --- |
| фронтматтеры | правка `*.md` или `*.json` | весь репозиторий | миллисекунды |
| копии правил | правка `*.md` | весь репозиторий | миллисекунды |
| адреса документов | **каждый коммит** | весь репозиторий | ~0.07 с |
| журнал решений | **каждый коммит** | весь репозиторий | ~0.09 с |
| диаграммы | правка `*.md` | staged-файлы | ~1 с на файл |
| `ruff check --fix` | правка `*.py` | staged-файлы | доли секунды |
| `pyrefly check` | правка `*.py` | staged-файлы | доли секунды |
Glob разводит две половины: коммит, трогающий одни скрипты, не платит за рендер
диаграмм, а коммит в документы не гоняет линтеры.
**Судятся staged-файлы, а не рабочее дерево** — гейт обязан проверять то, что
уедет в историю, а не то, что случайно лежит рядом на диске. Исключений три, и
все про существо, а не про удобство: `copies.py` сверяет копию с домом, а
дом лежит в другом файле, которого в индексе может не быть (список staged дал бы
«копии дословны» ровно там, где правка дома их и разошлась); `frontmatter.py`
обходит весь репозиторий за сотые доли секунды — экономить тут нечего;
`addresses.py` идёт **без glob вовсе**, потому что сводит две стороны: перечень
адресов лежит в `*.py` владельца, а упоминания — в `*.md` соседей, и коммит с
переименованием документа трогает только первую; `decisions.py` — по той же
причине: файл темы и ссылки на него лежат порознь, и переименование темы трогает
только одну сторону.
**Что судит `decisions.py`.** Номер записи журнала обязан быть один: буквенные
метки решений выродились до пятибуквенных и разъехались молча — `АЕАКЛ` означала
и тему 53, и тему 65. И подпись ссылки обязана называть свою цель: в
`[тема 5](decisions/05-project-start-lifecycle.md)` номер записан дважды, словом и путём, —
дубль неизбежный (иначе ссылку не прочитать, не открыв) и потому сверяемый, как
всякая копия.
**`ruff` чинит безопасное сам, и починка доносится до этого же коммита**
(`stage_fixed: true`). Иначе исправленный файл остался бы в рабочем дереве, а в
историю уехал бы невычищенный — худший из исходов: гейт зелёный, коммит грязный.
**`resync.py` в гейте нет намеренно** — он чинит, а не проверяет, и его правка
обязана быть прочитана глазами (см. выше).
**Обход разовый — `LEFTHOOK=0 git commit …`.** Он законен ровно для случая,
когда найденное нечем чинить прямо сейчас; молча пропущенная проверка — нет.
-105
View File
@@ -1,105 +0,0 @@
# Остатки, открытые вопросы и принятые пределы
Состояние на 2026-08-03, после разбора двенадцати тем и 16 коммитов реализации
(`ad1779b``885981c`).
План работ — [TODO.md](TODO.md). Решения с причинами — [DECISIONS.md](DECISIONS.md).
Здесь то, что **не** является работой из плана: незакрытые риски, честно принятые
пределы и вопросы, у которых пока нет ответа.
## Главный незакрытый риск
**Калибровка не сделана, а charter'ы переписаны трижды.**
Первый раз девять charter'ов правили при выносе в плагин: предмет проверки
заменили ссылкой на раздел брифа. `references/calibration.md` требует при такой
правке замерить, помогла ли она, — **замера не было**. Второй раз их переписали
коммитом `9cef452`: ссылка на раздел брифа заменена путём документа канона.
Третий — коммитами `0eab075` и следующим, по находкам ревью: `adversary`, `ops`,
`reimpl`, `rubric` и `triage` правились ещё раз.
**Три неизмеренных изменения подряд** в том самом месте, где присваивается
severity. Пробы готовы и синтетических не нужно — четыре реальные находки
прошедшей сессии healthlog:
- скелет из `null` затирает маршрут тренировки молча и необратимо;
- откат бинаря поверх новой схемы стартует без единого слова;
- канонизация внутри транзакции — 768 МиБ пика, 5.019 с удержания блокировки;
- `-1 >= -1` читается как «журнал разобран целиком».
Ожидаемый исход известен и его стоит проверить первым: метод переносится, а
**severity деградирует**. Третья находка без слота под представление данных и
настройки хранилища превращалась из `critical` с прогнанным оракулом в условное
наблюдение. Ровно ради этого случая канон развёл числа (`docs/research/`) и
настройки (`docs/database.md`) по разным домам и **обязал проход их сшивать**
но работает ли обязанность, не проверено. Оркестратор реагирует на severity,
поэтому цена — не «не найдём», а **«найдём и не починим»**.
Замер стоит перед переездом jellybit и блокирует его (решение 39).
## Что ещё не сделано
Список работ — в [TODO.md](TODO.md). Здесь только то, что стоит держать в голове
отдельно:
- **Плагины отправлены, но ни к одному проекту не подключены.** 17 коммитов
ушли на origin, клон маркетплейса обновлён до `88c5d97` и видит `av-dev-pm`
и `av-dev-pipeline` — то есть подключать теперь есть что. Первым делом это
делает healthlog, по разделу 2 плана.
- **Ни один скилл не прогонялся на живом проекте.** `docs.py` прогнан на
healthlog и jellybit в режиме `check` и находит осмысленный дрейф; `init`,
`canon adopt`, `canon upgrade` и скилл `docs` не исполнялись ни разу.
- **Проектные копии в healthlog и jellybit.** Два `.claude/skills/` и одиннадцать
`.claude/agents/` старого поколения. У jellybit хуже: его скиллы названы
`task-pipeline`, `review-pipeline`, `task-batch`**ровно как в плагине**.
Claude Code не переопределяет их, а держит обе пары, так что короткое имя может
увести в устаревшую копию, и молча.
## Открытые вопросы
**Как проверять, что канон не разошёлся с проектами после `upgrade`.** `canon
check` сверяет версию, но не то, что миграционные записи journal'а применены
верно. Проект может нести `"canon": 2` и не иметь того, что версия 2 требовала.
**Форма ADR при пересмотре решения.** Правило «старая запись получает статус
`заменено на`» требует, чтобы кто-то заметил, что новое решение отменяет старое.
Механической проверки нет, а принуждённое отрицание на шаге синка спрашивает про
`adr/` вообще, а не «не отменяет ли это что-то из существующего».
**Что делать с `av-dev-backlog` после перевода jellybit.** Помечен устаревшим и
переписан так, чтобы не ловить триггер. Удалять его из маркетплейса или оставить
как есть — решится, когда jellybit переедет.
## Известные пределы — приняты, чинить не планируется
**Транзакций на несколько файлов нет.** POSIX её не даёт без журнала. Окно сжато
до цепочки `rename` без ввода-вывода, а всё, что в окне может разъехаться,
сделано производным и восстанавливается `check --fix` без потерь.
**Оракул в критериях приёмки проверяется эвристикой.** Число пунктов проверяется
жёстко, наличие оракула — по слову, и это **только замечание**. В тексте прямо
сказано, что проверено меньше, чем требуется.
**Recall прохода по конвенциям равен качеству конвенций проекта.** Своего списка
у него нет: критерий берётся из `docs/conventions/`. На проекте с тонкими
конвенциями проход почти пуст, и charter это признаёт вслух.
**Доменного словаря в каноне нет.** Проходы получают факты, но не термины;
словарь строится каждый раз заново из спек и архитектуры. Цена не измерена.
**Смысловые дубли ловит только агент.** `docs.py` видит раскладку, но не то, что
`docs/specs/recognition.md` описывает то же, что capability `recognition`.
Граница объявляется вслух в каждом отчёте — это единственная защита от
«соблюдено» на проекте с тремя лишними файлами.
**Приёмщик и исполнитель совпали.** Граница «пайплайн не закрывает задачу» снята
сознательно (решение P); три защиты из раздела «Стимулы» держатся теперь текстом,
а не механикой. Реальные опоры — сохранённый отчёт триажа, `SPRINT.md` под git и
`reopen`. Это записано в самом скилле, а не спрятано.
**Копия правила в шаблонах проекта.** `adr/README.md` и `review.md` уезжают в
репозиторий и обязаны там что-то говорить, поэтому правило канона в них
копируется намеренно. Расхождение копии с домом ловит `scripts/copies.py`
но только у **помеченной** копии, и только внутри маркетплейса. Остаётся на
человеке двое: пометить копию и завести запись в журнал версий, когда правка
уже уехала в проект.
-183
View File
@@ -1,183 +0,0 @@
# Работы по итогам разбора
Порядок и обоснование — [DECISIONS.md](DECISIONS.md), тема 8. Номера в скобках —
следствия оттуда.
Замер (шаг 3) — **единственный шаг, который нельзя переставить**: он блокирует
переезд jellybit. Всё остальное можно тасовать.
## 0. Предусловие
- [x] `git push``092d07c..88c5d97`, 17 коммитов ушли на origin (37)
- [x] `claude plugin marketplace update av-dev-skills` — клон встал на `88c5d97`
и видит `av-dev-pm` и `av-dev-pipeline`
## 1. Репозиторий плагинов
### 1.1 Переименование
- [x] `av-dev-tasks``av-dev-pm`: каталог, `plugin.json`, `marketplace.json` (18)
- [x] пространство имён во всех текстах: `av-dev-tasks:session`
`av-dev-pm:session`, включая ссылку из `task-pipeline` (18)
### 1.2 Канон — единственный дом определения
- [x] `av-dev-pm/skills/canon/references/canon.md` — раскладка, роли документов,
правило единственного дома. Читают `init`, `canon`, `docs` (AA)
- [x] `av-dev-pm/skills/canon/references/changelog.md` — журнал версий канона,
версия 1 (26)
### 1.3 Правки существующих скиллов
- [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` и решить по
каталожной форме: жмёт → канон версии **4** для `architecture.md` и
`review.md`, точка входа `README.md` (тема 16, GGG, 65; версию 3 занял
роадмап с родом работы, тема 17, 68)
- [ ] завести `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)
- [ ] удалить проектные копии: `.claude/skills/healthlog-{task,review}-pipeline`
и девять `.claude/agents/healthlog-review-*.md` — они прошлого поколения и
после переезда указывают на `docs/conventions.md`, `docs/local-research.md`,
`docs/review-journal.md`, которых уже не будет
## 3. Калибровка — блокирует шаг 5
- [ ] замер на четырёх находках healthlog: скелет из `null`, откат бинаря,
канонизация в транзакции, `-1 >= -1` (1 из REMAINING, 14)
## 4. Обкатка
- [ ] один-два спринта healthlog на новом процессе
## 5. jellybit
- [ ] `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 → задачи
`[idea]`, logical-title-model → ADR (H)
- [ ] `docs/backlog/``docs/tasks/`
- [ ] удалить проектные копии скиллов и агентов (4 из REMAINING)
- [ ] `av-dev-backlog` удалить из маркетплейса
## 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`.
- [ ] healthlog: `PLAN.md``ROADMAP.md`, ссылки, `"canon": 3`
- [ ] jellybit: то же
- [ ] род работы и раздел «Затрагивает» — **не задним числом**: сперва то, что
идёт в ближайший набор (`sprint take` без них откажет), остальное по ходу
переоценки (PPP)
- [ ] секции роадмапа: `порядок``Запланировано`, `темы``Направления`,
завести `Готово` и `Разработка`; прозаические разделы healthlog («Что уже
пройдено», «Почему в таком порядке») разложить — звенья строками в
`Готово`, обоснование очереди прозой внутри `Запланировано` (тема 19, 80).
`check` теперь называет чужую секцию ошибкой, так что шаг обязателен
- [ ] переформулировать цели ответом на «что приложение будет уметь»; цели не
про приложение («Процесс и качество разработки» в jellybit) — в
`Разработка`
- [ ] `check --fix` на обоих: поднимет написание канонических секций, поставит
отбивку после заголовков и сведёт секцию в мете файлов с заголовками.
Секции беклога переименовать руками — имена выбирал проект (тема 20, ККК)
- [ ] заголовки задач в форму действия — **не задним числом**: по мере попадания
задачи в работу. `check` печатает их число, `task-form` предложит
формулировки пачкой (тема 20, ЕЕЕ)
@@ -1,8 +0,0 @@
{
"name": "av-dev-backlog",
"description": "УСТАРЕЛ, заменён плагином av-dev-pm. Старый формат беклога: один каталог задач с индексом README и приоритетами секциями, без целей и спринтов. Оставлен до перевода последнего проекта, который на нём ещё живёт; новые проекты не подключают.",
"author": {
"name": "Anton Vakhrushev",
"email": "anwinged@gmail.com"
}
}
-174
View File
@@ -1,174 +0,0 @@
---
name: backlog
description: "УСТАРЕЛ — используй скилл av-dev-pm:tasks. Старый формат беклога (один каталог задач, индекс README, приоритеты секциями, без целей и спринтов). Вызывать ТОЛЬКО в проекте, который на этот формат ещё не переведён, и только если прямо названо имя backlog. Во всех остальных случаях, включая любую просьбу завести задачу, идею или разобрать находки ревью, работает av-dev-pm:tasks."
---
> **Этот скилл устарел.** Формат заменён каноном `docs/tasks/` из плагина
> `av-dev-pm` (цели вместо приоритетов, спринт с заморозкой набора, `REJECTED.md`
> с причинами). Перевод проекта делает скилл `av-dev-pm:canon`. Скилл оставлен до
> перевода последнего проекта, который на нём ещё живёт, и будет удалён.
# Беклог
Беклог — каталог markdown-файлов: одна задача = один файл `<slug>.md`, плюс
строка в индексе `README.md`. Скилл ведёт беклог: заводит, чистит, приоритизирует,
дробит, штурмует идеи. **Реализацией не занимается** — это дело пайплайна задачи.
## Три правила, из которых всё следует
Ситуация не покрыта инструкцией — решай по ним.
1. **Беклог гниёт с той стороны, где его пополняют.** Заведение — самая частая
операция и с худшим отказом: из одного разговора рождается пять файлов, и
груминг потом разгребает то, чего не надо было заводить. Дедупликация и фильтр
на входе дешевле любой чистки. Заводим только то, что **не делаем сейчас** и о
потере чего пожалеем.
2. **Файл — источник истины, индекс производен.** Разошлись — неправ индекс.
Согласованность механизируема и проверяется командой, а не вниманием: всё, что
ловит `backlog.py check`, не должно попадать ни в чек-лист, ни в промпт.
3. **Причина переживает запись.** Приоритет без причины будет переспорен на
следующем груминге; выкинутая без причины задача вернётся через квартал тем же
текстом. Реализованная задача оставляет след в коммите и спеке — выкинутая не
оставляет ничего, поэтому у неё есть кладбище.
## Инструмент (`backlog.py`)
Пусть `bl="$CLAUDE_PLUGIN_ROOT/skills/backlog/scripts/backlog.py"`.
```
python3 $bl check # согласованность + метрики здоровья, exit 1 при расхождениях
python3 $bl check --fix # + починить безопасный дрейф (секция, заголовок, дубли)
python3 $bl list --stale # от самой залежавшейся; ещё --priority --type --tag
python3 $bl add --slug S --title T --priority P [--type idea|epic] [--hook H] [--reason R] [--tag a,b]
python3 $bl edit S [--title T] [--hook H] [--type idea|epic|task] # переименовать / сменить хук, тип
python3 $bl move S --priority P [--reason R] # перенести в другую секцию
python3 $bl close S --reason R # на кладбище + удалить (выкинута)
python3 $bl close S --implemented # просто удалить (реализована, есть коммит)
python3 $bl init [--sections "..."] # завести беклог в новом проекте
```
Тип задачи — английское ключевое слово `idea` / `epic` / `task` (как и прочие
токены команд); `task` префикса не несёт, `idea`/`epic` кодируются `[idea]`/
`[epic]` в заголовке. Текст задачи при этом русский.
**Мутации правят файл и индекс заодно** — руками строку индекса или мета-строку
не пиши, зови `add`/`edit`/`move`/`close`. Смена заголовка, хука или типа (в том
числе понижение задачи до `[idea]`) — это `edit`, а не ручная правка H1 и
индекса: `edit` держит их в синхроне. Механика (слаг в имени, секция по
приоритету, формат кладбища, экранирование ввода) не может рассогласоваться,
потому что её делает скрипт. Тело задачи скрипт не трогает — `add` кладёт
заголовок, мета-строку и плейсхолдер, а контекст, шаги и ссылки ты дописываешь
редактором (пока плейсхолдер на месте, `check` напоминает, что тело не дописано).
`check` — единственный судья согласованности; что именно он ловит, скажет его
вывод, здесь не пересказываем. Гоняй его **в начале сессии** и **после каждой
правки**, даже если правил мутациями: дрейф мог накопиться раньше. Накопившийся
дрейф чини `check --fix` — он детерминированно правит безопасное (секция по файлу,
заголовок из H1, дубли строк), а неоднозначное (ссылка на исчезнувший файл,
битые строки) выносит тебе. Это идёт строкой доклада.
Формат файла, мета-строки, слага, индекса и кладбища —
[references/task-format.md](references/task-format.md). Там же тест «готова к
взятию».
## Сценарии
### Завести задачу или идею из диалога
1. **Фильтр.** Делаем прямо сейчас — не заводим. Не пожалеем о потере — не
заводим. Родилось три кандидата — покажи их и спроси, какие заводить: молча
заведённая пачка и есть тот самый отказ из правила 1.
2. **Дедуп.** `list` плюс поиск по слагам, хукам и телам (`grep -ril`), **включая
`CLOSED.md`**. Нашлось в беклоге — **дописываем в существующий файл**, а не
заводим соседний. Нашлось на кладбище — покажи пользователю ту строку и что
изменилось с момента отказа: та же идея вернулась через диалог, а не через
ревью. Две задачи об одном — самая дорогая находка груминга.
3. **Тип по тесту готовности** (см. task-format): проходит — задача (`--type task`,
без префикса), не проходит — идея (`--type idea`), проходит по пользе, но не
делается одним заходом — эпик (`--type epic`, сперва декомпозиция).
4. `add --slug … --title … --priority … --hook …` (тип, причину, теги — по
месту). Хук отвечает «почему это в беклоге», а не пересказывает первый абзац.
Затем допиши тело файла.
5. `check`.
### Разобрать находки аудита или ревью
Ревью и аудиты — код-ревью, архитектурный проход, аудит безопасности — тоже
источник задач, но с зеркальной диалогу опасностью: не пять файлов из одной
мысли, а сорок файлов из сорока сырых находок. Защита та же, что в самом ревью:
кластеризация по причине, дедуп против беклога, находка без свидетельства → идея,
а не задача, и карта кластеров пользователю до создания файлов. Порядок и
отображение серьёзности — [references/from-review.md](references/from-review.md).
### Груминг
Интерактивная сессия порциями, по дате правки из git и с правилом остановки —
[references/grooming.md](references/grooming.md). Ключевое: перед вопросом
пользователю проверь по коду и спекам, не сделано ли уже попутно, — это самая
частая находка и она не требует ничьего решения.
### Приоритизация
Приоритет — грубая оценка «ценность / стоимость», не обязательство к порядку.
Никаких очков и часов: уровни те, что есть в секциях индекса.
- Меняешь уровень — `move <slug> --priority <новый> --reason <причина>`; причина
уезжает в мета-строку.
- Повышаешь — назови, **что именно эта задача обгоняет**. Повышение без
проигравшего это не приоритизация, а согласие с последним, кто говорил.
- Задача, давно лежащая в нижней секции и не двигавшаяся (по дате git), —
кандидат на кладбище, а не на новый круг «оставить как есть».
### Декомпозиция и штурм идеи
[references/split.md](references/split.md). Обе операции превращают одну запись в
несколько, и у обеих есть проверяемый тест: части должны **мерджиться порознь** и
**каждая давать видимую пользу**, а у штурма исход «выкинуть» — полноправный.
## Общее для всех сценариев
- **Кладбище.** Задача уходит из беклога без реализации → `close <slug> --reason
<причина>`: скрипт пишет строку в `CLOSED.md` (дата, слаг, заголовок, причина,
бывший приоритет) и удаляет файл со строкой индекса. Реализованные туда не идут
— у них есть коммит, спека и ADR; для них `close <slug> --implemented`.
- **Границы покрытия в отчёте.** Любая сессия груминга, приоритизации или штурма
заканчивается строкой «просмотрено N из M, не трогали — …». Отчёт без неё
сообщает «беклог разобран», не сообщая, какая его часть осталась нетронутой.
- **Развилки — пользователю.** Через `AskUserQuestion`, с уже сформулированным
предварительным суждением (рекомендация — первым вариантом). Что выкинуть, что
повысить, какая рамка идеи верна — решение пользователя. Слаг, формулировка,
порядок строк в индексе — механика, делаем сами.
- **Не больше трёх вопросов за раз.** Пачка длиннее трёх тяжела для ответа;
решений больше — веди **несколько итераций** диалога по ≤3, а не один
перегруженный запрос. Между итерациями применяй уже решённое.
- **Ничего не удаляем молча.** Файл задачи исчезает только через `close` —
`--reason` (выкинута) или `--implemented` (реализована). Прямого `rm` нет.
## Переносимость
Скилл независим от **языка программирования, сборки, CI и трекера**: он ничего не
знает ни про Go, ни про npm, ни про конкретный багтрекер — беклог для него просто
каталог markdown. Текст задач — русский (язык документации проекта); зашита только
латиница слага.
- **Каталог беклога**: аргумент → указатель в `CLAUDE.md` проекта → поиск
(`docs/backlog`, `backlog`, `doc/backlog`, `docs/tasks`). Не нашёлся — это новый
проект: `init` заводит индекс и кладбище (секции по умолчанию высокий/средний/
низкий, `--sections` переопределяет).
- **Слаг** — латиница kebab-case всегда; заголовок, тело, хук — по-русски.
- **Уровни приоритета** берутся из заголовков секций индекса как есть, их
количество и названия — дело проекта.
- **Имена служебных файлов** (`README.md` — индекс, `CLOSED.md` — кладбище)
фиксированы скиллом, не проектом.
Проектные тонкости (куда переезжает суть реализованной задачи, кто удаляет файл,
как беклог связан с трекером-инбоксом) описаны в `CLAUDE.md` проекта — прочитай
его перед работой.
## Чего этот скилл не делает
Не пишет код, не заводит спеки и change, не берёт задачу в работу — этим
занимается пайплайн задачи проекта, а этот скилл владеет только форматом и
содержимым беклога. Не решает за пользователя, что важно. Не переоформляет
существующие задачи «заодно»: правится то, чего касается операция.
@@ -1,84 +0,0 @@
# Задачи из аудита и ревью
Ревью и аудиты — код-ревью, архитектурный проход, аудит безопасности, любой
разбор другим агентом — порождают находки, часть которых становится задачами
беклога. Это отдельный интейк со своей опасностью, **зеркальной** интейку из
диалога.
- Интейк из диалога грешит переполнением: из одной мысли рождается пять файлов.
- Интейк из ревью грешит сваливанием: сорок сырых находок превращаются в сорок
файлов. Беклог раздувается, а следующий груминг склеивает их обратно.
Защита от сваливания — та же, что в самом ревью: **кластеризация по причине, а не
файл-на-находку.** Если у ревью был триаж — половина работы уже сделана, бери его
выход. Если нет — триажируй сам, прежде чем заводить.
## Находка агента — не задача
Мнение агента — **гипотеза, пока у неё нет свидетельства** (падающий тест,
воспроизводимый шаг, положение гайда). Согласие нескольких находок само по себе
достоверность не повышает: это один источник, высказавшийся несколько раз.
Отсюда фильтр входа, поверх обычного «не делаем сейчас + пожалеем о потере»:
- **Находка со свидетельством**, отложенная к исполнению → **задача**.
Свидетельство и последствие переносим в тело — это её «почему», то самое, что
переживает запись.
- **Находка без свидетельства / низкой уверенности** → **идея** (`[idea]`), а не
задача. Она не заработала приоритизацию: сравнивать неподтверждённое не с чем.
Её судьба — штурм, где либо найдётся подтверждение, либо она уедет на кладбище.
- **Уже починено по ходу ревью** → **ничего**. Починенное не заводим.
- **Развилка, решённая при ревью** → ничего; решённая «потом» → задача с
зафиксированным вопросом.
## Порядок
1. **Возьми выход триажа, а не сырые находки.** Сырой отчёт — это симптомы до
дедупликации; в нём одна причина размазана по нескольким строкам.
2. **Кластеризуй по причине.** Пять находок об одном отсутствующем инварианте —
одна задача, а не пять. Класс мелочи (nits, косметика) — **один пакетный файл**
со списком пунктов, а не файл на каждую запятую.
3. **Дедуп против беклога и кладбища.** Аудит переоткрывает уже заведённое и уже
выкинутое. Нашлось в беклоге — дописываем находку в существующий файл. Нашлось
на кладбище — это сигнал: причина отказа могла устареть, выноси пользователю, а
не заводи молча заново.
4. **Покажи карту до создания файлов.** Кластер → задача / идея / строка в
пакетный файл / уже в беклоге / отброшено — пачкой через `AskUserQuestion`.
Это тот же барьер, что и «три кандидата» в интейке из диалога: массовое
заведение файлов без подтверждения — ровно тот отказ, ради которого интейк из
ревью и выделен. Дешёвая мелочь по явному согласию может заводиться и без
поштучного вопроса — но карта пользователю всё равно предъявляется.
5. **Заводи утверждённое** через `backlog.py add`, с двумя добавками:
- **тег партии** — `add … --tag review-ГГГГ-ММ-ДД` (или `audit-<тема>`), чтобы
весь заход груминга поднимался одной командой `backlog.py list --tag …`;
- **провенанс в теле** — кто нашёл, каким проходом, с каким свидетельством. Без
него через месяц не отличить проверенную находку от догадки.
6. `backlog.py check`.
## Отображение серьёзности на приоритет
Правило концептуальное, от полей конкретного отчёта не зависит:
- **выше серьёзность → выше приоритет.** Самый тяжёлый класс находок → верхняя
секция индекса, следующий → следующая. Отображать словарь серьёзности отчёта на
словарь приоритетов проекта точно нечем — при сомнении спрашивай пользователя.
- **низкая уверенность или нет свидетельства → идея**, не задача.
- **мелочь → строка в пакетный файл**, не отдельный.
- **уже починено / развилка решена сейчас → ничего.**
Если у ревью структурированный отчёт с полями серьёзности, уверенности,
свидетельства и предписанного действия (например, конвейер ревью jellybit даёт
`Severity`/`Confidence`/`Оракул`/`Действие: инлайн|развилка`) — правило выше
ложится на эти поля механически. Но это пример одного формата, а не требование к
источнику: тот же фильтр применяется к находкам в свободной форме.
Границы покрытия отчёта — то, что ревью проверить **не смогло**, — не находки и в
задачи не идут: у них нет предмета. Их место — в докладе, не в беклоге.
## Доклад
- Источник (какое ревью/аудит, сколько находок на входе).
- Свёрнуто в задачи: N кластеров из M находок, со слагами и тегом партии.
- Что не заведено и почему: починено инлайн, уже в беклоге, ушло в идеи, на
кладбище.
- `backlog.py check`.
@@ -1,101 +0,0 @@
# Груминг беклога
Цель — выкинуть то, что перестало быть задачей, и вернуть остальному честное
состояние. Не «пересмотреть всё», а «пересмотреть порцию до конца».
## Порция и правило остановки
Тридцать задач за один заход — это усталость и штамповка: последние десять
получат «оставить» не потому, что живы, а потому, что сессия затянулась.
- **5–8 задач за сессию.** Больше — только если пользователь настаивает, и тогда
разбей на явные порции с промежуточным докладом.
- **Отбор порции** — один из:
- `backlog.py list --stale` — самые залежавшиеся по дате последней правки в
git; поле «дата касания» заводить не надо, git её уже хранит;
- одна секция приоритета целиком;
- один тег (`--tag`) — например, задачи, пришедшие из одного ревью;
- список от пользователя.
- **Останавливайся на границе порции**, даже если «ещё чуть-чуть осталось».
## Что делать с каждой задачей
Сперва то, что не требует ничьего решения:
1. **Проверь, не сделано ли уже.** Задача, реализованная попутно в соседнем
изменении, — самая частая находка груминга. Смотри код, спеки, историю
коммитов по ключевым словам задачи. Удаление задачи «как реализованной» —
деструктивно и без следа (кладбище для реализованных не пишется), поэтому
порог улики жёсткий: удаляем (`close <slug> --implemented`), только имея
**конкретный коммит или строку спеки**, закрывающие задачу, и ссылка на них
идёт в доклад. Есть лишь косвенные признаки — не удаляй сам, вынеси в пачку
вопросов. Сделана частично → задача сжимается до остатка: тело правишь
редактором, заголовок и хук — через `edit <slug> --title … --hook …`.
2. **Проверь, не отменена ли решением.** ADR, спека или архивный change мог
закрыть вопрос иначе. Тогда `close <slug> --reason "<ссылка на решение>"`.
3. **Проверь пересечения внутри порции.** Две задачи об одном — содержимое в
одну, вторую `close <slug> --reason "слита с <другой-slug>"`.
Затем — то, что решает пользователь:
4. **Жива ли она вообще.** Контекст мог измениться: ушла зависимость, отпал
сценарий, обошли иначе. Здесь и звучит вопрос о выкидывании.
5. **Тот ли приоритет** (тест и правила — в SKILL.md и task-format.md).
6. **Задача ли это по-прежнему.** Не проходит тест «готова к взятию» → `edit
<slug> --type idea`, и её дальнейшая судьба — штурм, а не приоритизация.
Разрослась → `edit <slug> --type epic`, дальше декомпозиция.
## Храповик
Сильно залежавшаяся задача — сигнал сама по себе: её либо ни разу не собирались
делать, либо нечем взять. Измеряй наблюдаемым — датой последней правки из git
(`backlog.py list --stale` ставит такие первыми); счётчик «сколько грумингов
пережила» нигде не хранится, поэтому на него не опирайся.
Задача из верхних строк `--stale`, которую и этот заход оставляет без изменений,
**либо двигается (вверх или на кладбище), либо остаётся с явно записанной
причиной**, почему её держим (`move <slug> --priority <тот же> --reason …`).
Молчаливое «оставить как есть» на давно неподвижной задаче — это решение не
принимать решение; запись причины превращает его в осознанное и не даёт тому же
вопросу всплыть на следующем груминге. В примере ниже вариант «оставить» именно
такой — с названной причиной, а не по умолчанию.
## Интерактив
- Вопросы — через `AskUserQuestion`, **не больше трёх за раз**. Порция в 5–8
задач обычно даёт больше трёх суждений — тогда веди несколько итераций по ≤3, а
не по одному на задачу и не одним перегруженным запросом.
- К каждому варианту — **предварительное суждение**, рекомендация первым
вариантом: «предлагаю выкинуть, потому что …». Пользователю дешевле возразить,
чем судить с нуля.
- Всё, что решается фактом (сделано / отменено / дублируется), решай сам и
показывай списком в докладе, а не выноси в вопросы.
Пример одной итерации — три залежавшихся задачи, механику по ним уже разобрали:
> **Груминг: 3 залежавшихся (порция по `--stale`)**
>
> 1. `versii-kachestvo-repaki` — репаки, апгрейд 1080p→2160p
> - Выкинуть на кладбище *(рекомендую)* — помечена «не боль», за полгода ни разу не возникла
> - Оставить в низком
> - Поднять в средний
> 2. `backup-sqlite` — бэкап SQLite
> - Оставить в среднем *(рекомендую)* — не сработала, но риск реальный
> - Поднять в высокий — обгоняет `retention-ochistka-bd`: без бэкапа ретеншн опасен
> - Выкинуть
> 3. `guessit-sputnik` — guessit как сервис-спутник
> - Понизить до `[idea]` *(рекомендую)* — не проходит тест «готова к взятию»
> - Оставить задачей в низком
Каждый вариант несёт причину — ту самую, что уедет в `move --reason` или
`close --reason`. Ответы применяй сразу и, если в порции осталось ещё, следующей
итерацией показывай следующие ≤3.
## Доклад
- Что просмотрено: N из M, по какому признаку отобрана порция.
- Изменения списком: удалено (реализовано), на кладбище (с причинами), понижено
до идей, слито, переприоритизировано.
- **Границы покрытия**: сколько задач не трогали и какие именно секции или теги
остались — иначе доклад читается как «беклог разобран».
- `backlog.py check` после правок; результат — строкой в докладе.
@@ -1,62 +0,0 @@
# Декомпозиция и мозговой штурм
Обе операции превращают одну запись беклога в несколько (или в ноль). Разница в
входе: декомпозиция дробит **готовую задачу**, штурм прорабатывает **идею**,
которая ещё не задача.
## Тест декомпозиции
Задачу можно дробить, только если части удовлетворяют **обоим** условиям:
1. **Мерджатся независимо.** Часть Б не требует, чтобы часть А была уже влита.
Есть порядок «сперва А, потом Б, иначе не собрать» → это не декомпозиция, а
план реализации: шаги остаются **внутри одного файла** в разделе «Шаги».
2. **Каждая даёт видимую пользу.** Часть, полезная только в комплекте с другой, —
не самостоятельная задача. Пользу проверяй тем же тестом «готова к взятию»
(task-format): что станет наблюдаемо иначе именно от этой части.
Не проходит хотя бы одно — **не дроби**. Ложная декомпозиция плодит файлы,
которые нельзя взять поодиночке, и груминг потом их склеивает обратно.
## Что делать с родителем
После разделения родитель **не остаётся** третьей висящей строкой:
- части полностью замещают его → `close <slug> --reason "разложена на a, b"`.
Кладбище здесь — не «выкинули», а именно тот след, что переживает запись:
через квартал вопрос «куда делась задача X» отвечается строкой кладбища со
ссылками на наследников, а не археологией git;
- родитель осмыслен как зонтик → `edit <slug> --type epic`, тело — ссылки на
задачи-части, своих шагов у него нет.
Одно и то же не должно лежать и в родителе, и в части. Задвоение — то же
расхождение, что ловит `check`, только внутри тел.
## Мозговой штурм идеи
Идея (`[idea]`) не проходит тест «готова к взятию»: неясно, что именно делаем.
Штурм проясняет — и это **generative-операция, а не applicative**.
Applicative-штурм («перечисли задачи, следующие из идеи») выдаёт очевидное:
перечисляется то, что уже видно в формулировке. Ценное — на уровень выше.
1. **Сперва — формы, а не задачи.** Предложи **три разные постановки** идеи и
назови **компромисс каждой**: что она даёт, чем платит, что оставляет за
бортом. Если получилась одна постановка — штурм не состоялся, это applicative.
2. **Вынеси формы пользователю** через `AskUserQuestion` с компромиссами. Рамку
выбирает он: это продуктовое решение, не механика.
3. **Только выбранную форму** дроби по тесту декомпозиции выше.
**«Выкинуть» — полноправный исход штурма, а не его неудача.** Проработка, честно
показавшая, что пользы нет или она несоразмерна цене, — это результат: идея
уезжает на кладбище с этой самой причиной, и та причина гасит её повторное
появление.
## Доклад
- Идея/задача на входе, выбранная рамка (для штурма), задачи-наследники со
слагами и приоритетами.
- Судьба родителя: удалён / стал эпиком / выкинут.
- `backlog.py check` после правок.
- Границы покрытия: какие постановки рассмотрены и какие сознательно отброшены —
чтобы штурм не пришлось повторять с нуля.
@@ -1,104 +0,0 @@
# Формат беклога
Заголовок, мета-строку и строку индекса ставит `backlog.py add` — руками их не
пишут. Этот файл описывает, что именно скрипт создаёт и что проверяет `check`;
тело задачи (контекст, шаги, ссылки) дописывает агент.
## Файл задачи
`<slug>.md` в каталоге беклога:
```markdown
# Раздачи с докачиванием (merge при повторном добавлении)
**Приоритет:** высокий — блокирует типовой сценарий свежих сериалов · **Теги:** layout, ingest
Свежий сериал раздают по мере выхода: торрент с 5 из 10 эпизодов позже
перезаливают целиком, пользователь добавляет раздачу повторно. …
Шаги:
- в плане раскладки отличать «путь занят живой ссылкой того же матча» от коллизии
- merge-раскладка: существующее пропустить, недостающее доложить
Зависит от правила сходимости. Связано: drafts/logical-title-model.md §6.2.
```
- **Заголовок H1** — он же заголовок строки в индексе, дословно. Тип кодируется
префиксом `[idea]` / `[epic]`; обычная задача без префикса. Отдельного поля
типа **нет**: два места для одного факта разъезжаются, а префикс виден прямо в
индексе, где и принимается решение «брать или не брать».
- **Мета-строка** — первая непустая строка после заголовка. Обязателен приоритет,
причина после тире желательна, теги опциональны. Поля разделяются ` · `, их
порядок свободный. `·` — служебный разделитель: в тексте причины его быть не
должно, иначе причина обрежется по нему.
- **Тело** — контекст (почему это вообще задача), принятые решения, шаги,
ссылки на спеки, ADR, черновики, прошлые ревью. Пишется на языке документации
проекта.
Тело — не план реализации и не спецификация: принятое и реализованное переезжает
в документацию проекта, а файл задачи удаляется.
## Слаг
Латиница и цифры, kebab-case, без ведущих, хвостовых и двойных дефисов
(`foo-bar`, не `-foo`, `a--b`). Именуется **по сути задачи, а не по текущей
формулировке**: заголовок будет переписан на груминге, а слаг стоит в ссылках из
других задач, коммитов и черновиков. Транслит русского названия допустим, если
суть иначе не выражается коротко.
## Индекс
`README.md` в том же каталоге: преамбула, затем секции по приоритетам, в каждой —
строки вида
```markdown
- [Заголовок задачи дословно](slug.md) — хук
```
Хук отвечает на «почему это лежит в беклоге» одним предложением: состояние,
остаток, боль. Пересказ первого абзаца бесполезен — он уже есть по ссылке.
Порядок секций задаёт порядок приоритетов, их названия — единственный словарь
уровней. Внутри секции порядок значения не имеет. Секции приоритетов — **единственные
заголовки `##` в индексе**: любой другой `##` в преамбуле проверка сочтёт уровнем
приоритета.
Индекс **производен**: расходится с файлом — правим индекс. Строку индекса руками
не пишут — её ставит `backlog.py add` в секцию приоритета и двигает `move`.
## Кладбище — `CLOSED.md`
Туда уходит задача, покинувшая беклог **без реализации**. Строку пишет
`backlog.py close --reason`, а `check` следит за её форматом:
```markdown
- 2026-07-23 `versii-kachestvo-repaki` — Версии/качество одного тайтла (репаки,
апгрейд 1080p → 2160p). Причина: калибровка болей — не боль, ни разу не
возникло за полгода. Был приоритет: низкий.
```
Реализованные сюда не попадают: у них остаётся коммит, спека, ADR. У выкинутой не
остаётся ничего — и через квартал она возвращается тем же текстом через инбокс.
Кладбище — первое место, куда смотрит дедупликация при заведении.
Запись на кладбище не запрещает завести задачу заново: изменился контекст —
заводим и ссылаемся на строку кладбища, объясняя, что изменилось.
## Тест «готова к взятию»
Задача готова, если из файла отвечаются три вопроса:
1. **Что станет наблюдаемо иначе**, когда она сделана — снаружи: пользователю,
владельцу сервиса или разработчику. «Отрефакторить X» — не ответ; «перестанет
ломаться Y при Z» — ответ.
2. **По чему видно, что закончено.** Признак завершённости, а не список работ.
3. **Почему приоритет такой** — одна строка.
Не отвечается первый или второй вопрос → это **идея**, её место в штурме, а не в
приоритизации. Приоритизировать идеи бессмысленно: сравнивается неизвестно что.
Отвечается всё, но задача не делается одним заходом и не мерджится целиком →
**эпик**, сперва декомпозиция.
Тест применяется при заведении и на груминге. К старым задачам, которых операция
не касается, задним числом не применяется — беклог не переоформляют «заодно».
@@ -1,706 +0,0 @@
#!/usr/bin/env python3
"""Детерминированный инструмент беклога: файлы задач против индекса README.
Согласованность беклога — механизируемая вещь, и держать её вниманием агента
дорого и ненадёжно. Скрипт не только проверяет, но и **пишет**: создание,
переименование, перенос между приоритетами и закрытие правят файл и индекс
заодно, так что рассогласовать их вручную нельзя. Всё, что здесь механизировано,
не должно попадать ни в промпт, ни в чек-лист человека.
Источник истины — файл задачи. Индекс производен от файлов: расходятся —
неправ индекс.
Тип задачи — ключевое слово (idea | epic | task); по-английски, как и прочие
токены команд. Обычная задача (task) префикса не несёт, idea/epic кодируются
префиксом `[idea]`/`[epic]` в заголовке. Текст самой задачи — русский.
Использование:
backlog.py check [--dir DIR] [--fix] согласованность (+ здоровье беклога);
--fix чинит безопасный дрейф
backlog.py list [--dir DIR] [фильтры] список задач
--stale от самой залежавшейся (дата последней правки из git)
--priority СЛОВО / --type idea|epic / --tag СЛОВО фильтры
backlog.py add --slug S --title T --priority P [--type idea|epic]
[--hook H] [--reason R] [--tag a,b] [--dir DIR]
создать задачу: файл + строка индекса
backlog.py edit S [--title T] [--hook H] [--type idea|epic|task] [--dir DIR]
сменить заголовок/хук/тип (файл + индекс)
backlog.py move S --priority P [--reason R] [--dir DIR]
перенести в другую секцию приоритета
backlog.py close S (--reason R | --implemented) [--dir DIR]
закрыть: --reason → кладбище + удаление,
--implemented → просто удаление (есть коммит)
backlog.py init [--dir DIR] [--sections "высокий,средний,низкий"]
завести пустой беклог в новом проекте
Тело задачи (контекст, шаги, ссылки) остаётся агенту — add кладёт лишь заголовок,
мета-строку и плейсхолдер; агент дописывает тело редактором.
Границы безопасности: слаг — только латиница kebab-case (traversal невозможен),
--dir обязан быть внутри рабочего каталога, в заголовок/хук/причину не пролезет
перевод строки, `·` в причине запрещён (это разделитель мета-полей).
Язык не зашит инструментально: приоритеты сопоставляются с заголовками секций
индекса как есть. Текст задач — русский.
"""
import argparse
import datetime
import os
import re
import subprocess
import sys
from pathlib import Path
INDEX = "README.md"
CLOSED = "CLOSED.md"
SERVICE = {INDEX, CLOSED}
META_FIELD = re.compile(r"^\*\*(.+?):\*\*\s*(.*)$")
INDEX_ENTRY = re.compile(r"^- \[(.+?)\]\((.+?\.md)\)\s*(?:—\s*(.*))?$")
SECTION = re.compile(r"^##\s+(.+?)\s*$")
TYPE_PREFIX = re.compile(r"^\[(.+?)\]\s*(.*)$")
SLUG_RE = re.compile(r"[a-z0-9]+(?:-[a-z0-9]+)*")
SLUG = re.compile(SLUG_RE.pattern + r"\.md")
# Строка кладбища: - ГГГГ-ММ-ДД `slug` — текст
CLOSED_ENTRY = re.compile(r"^- \d{4}-\d{2}-\d{2} `[a-z0-9-]+` — .+")
TYPES = ("idea", "epic") # непустые типы-ключевые слова, префикс [..] в H1
PLAIN_TYPE = "task" # обычная задача — без префикса
STALE_DAYS = 180 # порог «залежалась» для метрики здоровья в check
# --- Валидация недоверенного ввода (аргументы могут прийти из текста задачи) ---
def bad_line(value: str, field: str) -> str | None:
"""Однострочность: перевод строки/управляющий символ ломает индекс и файл."""
if value is not None and (any(c in value for c in "\n\r") or any(ord(c) < 32 for c in value)):
return f"{field}: перевод строки или управляющий символ запрещён"
return None
def bad_slug(slug: str) -> str | None:
if not SLUG_RE.fullmatch(slug):
return f"слаг «{slug}» — только латиница kebab-case (без ../, точек, слэшей)"
return None
def bad_reason(reason: str | None) -> str | None:
if reason is None:
return None
if (e := bad_line(reason, "причина")):
return e
if "·" in reason:
return "причина: символ · зарезервирован под разделитель мета-полей"
return None
def dir_within_cwd(root: Path) -> bool:
try:
root.resolve().relative_to(Path.cwd().resolve())
return True
except ValueError:
return False
# --- Атомарная запись: падение посреди write не оставит усечённый индекс ---
def write_atomic(path: Path, text: str) -> None:
tmp = path.with_name(path.name + ".tmp")
tmp.write_text(text, encoding="utf-8")
os.replace(tmp, path)
def resolve_dir(explicit: str | None) -> Path:
"""Каталог беклога для команд, кроме init. Явный --dir обязан быть внутри cwd."""
if explicit:
root = Path(explicit)
if not dir_within_cwd(root):
sys.exit(f"--dir вне рабочего каталога: {explicit}")
if not (root / INDEX).is_file():
sys.exit(f"беклога нет в «{explicit}» (нет {INDEX}); новый проект — backlog.py init")
return root
for candidate in ("docs/backlog", "backlog", "doc/backlog", "docs/tasks"):
if (Path(candidate) / INDEX).is_file():
return Path(candidate)
sys.exit("каталог беклога не найден, укажи --dir"
" (искал: docs/backlog, backlog, doc/backlog, docs/tasks)")
def parse_index(root: Path) -> tuple[dict[str, dict], list[str]]:
"""Строки индекса по имени файла + порядок секций (он же порядок приоритетов).
Дубли имени файла тут схлопываются (побеждает последний) — их отдельно ловит
index_lint, поэтому опираться на этот dict как на полноту нельзя.
"""
entries: dict[str, dict] = {}
sections: list[str] = []
section = None
for num, line in enumerate((root / INDEX).read_text(encoding="utf-8").splitlines(), 1):
m = SECTION.match(line)
if m:
section = m.group(1)
sections.append(section)
continue
m = INDEX_ENTRY.match(line)
if m:
title, target, hook = m.group(1), m.group(2), (m.group(3) or "").strip()
entries[target] = {"title": title, "section": section, "hook": hook, "line": num}
return entries, sections
def index_lint(root: Path) -> list[str]:
"""Структурные дефекты индекса, которые схлопнутый dict parse_index не видит:
битые строки-пункты, дубли на один файл, задачи до первой секции приоритета."""
errors: list[str] = []
section = None
seen: dict[str, int] = {}
for num, line in enumerate((root / INDEX).read_text(encoding="utf-8").splitlines(), 1):
if SECTION.match(line):
section = SECTION.match(line).group(1)
continue
if not line.startswith("- ["):
continue
m = INDEX_ENTRY.match(line)
if not m:
errors.append(f"{INDEX}:{num}: строка-пункт не по формату"
f" «- [Заголовок](slug.md) — хук»")
continue
target = m.group(2)
if section is None:
errors.append(f"{INDEX}:{num}: {target} стоит до первой секции приоритета")
if target in seen:
errors.append(f"{INDEX}:{num}: дубль строки для {target}"
f" (первая — строка {seen[target]})")
else:
seen[target] = num
return errors
def parse_task(path: Path) -> dict:
text = path.read_text(encoding="utf-8")
lines = text.splitlines()
title = lines[0].removeprefix("#").strip() if lines and lines[0].startswith("#") else ""
kind, bare = PLAIN_TYPE, title
m = TYPE_PREFIX.match(title)
if m:
kind, bare = m.group(1).strip().lower(), m.group(2).strip()
# Мета-строка — первая непустая строка после заголовка (task-format.md).
# Поля разделены `·`, порядок свободный: приоритет распознаётся, где бы он ни
# стоял, а не только первым. Причина не должна содержать `·` — это разделитель.
meta = next((ln.strip() for ln in lines[1:] if ln.strip()), "")
priority, reason, tags = "", "", []
if META_FIELD.match(meta):
for chunk in meta.split("·"):
f = META_FIELD.match(chunk.strip())
if not f:
continue
key, value = f.group(1).strip().lower(), f.group(2).strip()
if key in ("приоритет", "priority"):
priority, _, reason = (p.strip() for p in value.partition(""))
priority = priority.rstrip(".,").lower()
elif key in ("теги", "tags"):
tags = [t.strip().lower() for t in value.split(",") if t.strip()]
return {"title": title, "bare": bare, "type": kind, "priority": priority,
"reason": reason, "tags": tags, "path": path}
def tasks_of(root: Path) -> dict[str, dict]:
return {p.name: parse_task(p) for p in sorted(root.glob("*.md")) if p.name not in SERVICE}
def touched_map(root: Path) -> dict[str, str]:
"""Дата последнего коммита для каждого файла беклога — одним вызовом git.
Ключ — имя файла (в каталоге беклога имена уникальны). Нет git / нет
истории → пустая карта, вызывающий подставит «—»."""
try:
out = subprocess.run(["git", "log", "--format=%as", "--name-only", "--", str(root)],
capture_output=True, text=True).stdout
except FileNotFoundError:
return {}
dates: dict[str, str] = {}
cur = None
for line in out.splitlines():
if not line.strip():
continue
if re.fullmatch(r"\d{4}-\d{2}-\d{2}", line):
cur = line # лог новейшие сверху → первая дата и есть последняя правка
elif cur:
dates.setdefault(os.path.basename(line), cur)
return dates
def check(root: Path, fix: bool = False) -> int:
if fix:
for line in apply_fixes(root):
print(f"ПОЧИНЕНО {line}")
print()
entries, sections = parse_index(root)
tasks = tasks_of(root)
known = {s.lower() for s in sections}
errors: list[str] = []
notes: list[str] = []
for name, task in tasks.items():
entry = entries.get(name)
if not entry:
errors.append(f"{name}: файла нет в индексе {INDEX}")
if not SLUG.fullmatch(name):
errors.append(f"{name}: слаг не kebab-case латиницей")
if not task["title"]:
errors.append(f"{name}: нет заголовка H1")
if not task["priority"]:
errors.append(f"{name}: нет строки **Приоритет:**")
elif task["priority"] not in known:
errors.append(f"{name}: приоритет «{task['priority']}» не совпадает"
f" ни с одной секцией индекса ({', '.join(sections)})")
elif entry and entry["section"] and entry["section"].lower() != task["priority"]:
errors.append(f"{name}: приоритет в файле «{task['priority']}»,"
f" а в индексе секция «{entry['section']}»")
if entry and entry["title"] != task["title"]:
errors.append(f"{name}: заголовок разошёлся\n"
f" файл: {task['title']}\n"
f" индекс: {entry['title']}")
if entry and not entry["hook"]:
notes.append(f"{name}: строка индекса без хука — по ней не выбрать задачу")
if task["type"] not in TYPES and task["type"] != PLAIN_TYPE:
notes.append(f"{name}: тип «{task['type']}» вне словаря"
f" ({'/'.join(TYPES)} или без префикса)")
if "<!-- контекст" in task["path"].read_text(encoding="utf-8"):
notes.append(f"{name}: тело не дописано (остался плейсхолдер add)")
for name, entry in entries.items():
if name not in tasks:
errors.append(f"{INDEX}:{entry['line']}: ссылка на несуществующий {name}")
errors += index_lint(root)
# Кладбище: строки-пункты должны совпадать с форматом (его пишет close).
closed = root / CLOSED
if closed.is_file():
for num, line in enumerate(closed.read_text(encoding="utf-8").splitlines(), 1):
if line.startswith("- ") and not CLOSED_ENTRY.match(line):
errors.append(f"{CLOSED}:{num}: строка кладбища не по формату"
f" «- ГГГГ-ММ-ДД `slug` — …»")
# Причина у приоритета желательна, но не обязательна. Ругаемся только на
# частичное покрытие — это дрейф: у части задач причина есть, у части нет.
# Ноль из N — осознанный отказ проекта от причин, не расхождение; горящее на
# каждом check замечание агент просто научится игнорировать.
with_reason = sum(1 for t in tasks.values() if t["reason"])
if 0 < with_reason < len(tasks):
notes.append(f"причина у приоритета есть у {with_reason} из {len(tasks)}"
f" — либо у всех, либо ни у кого: вперемешку это дрейф")
print(f"беклог: {root}, задач {len(tasks)}, строк индекса {len(entries)},"
f" секций {len(sections)}")
health(root, tasks, sections)
for e in errors:
print(f"ОШИБКА {e}")
for n in notes:
print(f"замечание {n}")
if errors:
print(f"\nрасхождений: {len(errors)}")
return 1
print("\nиндекс согласован" + (f", замечаний: {len(notes)}" if notes else ""))
return 0
def health(root: Path, tasks: dict[str, dict], sections: list[str]) -> None:
"""Метрики здоровья беклога: размер секций и число давно неподвижных задач.
Механизирует правило «беклог гниёт со стороны пополнения» — раньше оно
держалось только на дисциплине."""
by_section = {s.lower(): 0 for s in sections}
for t in tasks.values():
if t["priority"] in by_section:
by_section[t["priority"]] += 1
sizes = ", ".join(f"{s} {by_section[s.lower()]}" for s in sections)
print(f" секции: {sizes}")
dates = touched_map(root)
if not dates:
return
cutoff = (datetime.date.today() - datetime.timedelta(days=STALE_DAYS)).isoformat()
stale = sum(1 for t in tasks.values()
if (d := dates.get(t["path"].name)) and d < cutoff)
if stale:
print(f" залежалось (>{STALE_DAYS} дней без правки): {stale}"
f" — груминг просрочен, начни с `list --stale`")
def list_tasks(root: Path, args: argparse.Namespace) -> int:
tasks = tasks_of(root)
_, sections = parse_index(root)
order = {s.lower(): i for i, s in enumerate(sections)}
rows = [t for t in tasks.values()
if (not args.priority or t["priority"] == args.priority.lower())
and (not args.type or t["type"] == args.type.lower())
and (not args.tag or args.tag.lower() in t["tags"])]
if args.stale:
dates = touched_map(root)
for t in rows:
t["touched"] = dates.get(t["path"].name, "")
rows.sort(key=lambda t: (t["touched"] == "", t["touched"]))
else:
rows.sort(key=lambda t: (order.get(t["priority"], 99), t["path"].name))
for t in rows:
touched = f"{t.get('touched', ''):<11}" if args.stale else ""
kind = "" if t["type"] == PLAIN_TYPE else f"[{t['type']}] "
print(f"{touched}{t['priority']:<9} {t['path'].stem:<46} {kind}{t['bare']}")
print(f"\nвсего: {len(rows)}")
return 0
# --- Мутации: правят файл и индекс заодно, чтобы их нельзя было рассогласовать ---
def fail(msg: str) -> int:
print(f"ошибка: {msg}", file=sys.stderr)
return 2
def build_meta(priority: str, reason: str, tags: list[str]) -> str:
s = f"**Приоритет:** {priority}"
if reason:
s += f"{reason}"
if tags:
s += " · **Теги:** " + ", ".join(tags)
return s
def load_index(root: Path) -> list[str]:
return (root / INDEX).read_text(encoding="utf-8").splitlines()
def save_index(root: Path, lines: list[str]) -> None:
write_atomic(root / INDEX, "\n".join(lines) + "\n")
def section_headers(lines: list[str]) -> list[tuple[int, str]]:
return [(i, m.group(1)) for i, l in enumerate(lines) if (m := SECTION.match(l))]
def find_section(lines: list[str], priority: str) -> tuple[int | None, str]:
for i, name in section_headers(lines):
if name.lower() == priority.lower():
return i, name
return None, ""
def find_entry_index(lines: list[str], slug: str) -> int | None:
for i, l in enumerate(lines):
m = INDEX_ENTRY.match(l)
if m and m.group(2) == f"{slug}.md":
return i
return None
def insert_entry(lines: list[str], section: str, entry: str) -> None:
"""Вставляет строку в конец секции (перед следующим ## или концом файла)."""
hi, _ = find_section(lines, section)
end = next((j for j in range(hi + 1, len(lines)) if SECTION.match(lines[j])), len(lines))
ins = end
while ins - 1 > hi and not lines[ins - 1].strip():
ins -= 1
lines.insert(ins, entry)
def update_priority(path: Path, priority: str, new_reason: str | None) -> bool:
"""Хирургически меняет только поле **Приоритет:** в мета-строке файла,
сохраняя теги, регистр и любые нераспознанные поля. Возвращает False, если
мета-строки нет (тогда правку делать нельзя — вызывающий падает)."""
flines = path.read_text(encoding="utf-8").splitlines()
mi = next((i for i in range(1, len(flines)) if flines[i].strip()), None)
if mi is None or not META_FIELD.match(flines[mi].strip()):
return False
chunks = flines[mi].split("·")
for idx, chunk in enumerate(chunks):
f = META_FIELD.match(chunk.strip())
if not (f and f.group(1).strip().lower() in ("приоритет", "priority")):
continue
_, _, old_reason = (p.strip() for p in f.group(2).partition(""))
reason = new_reason if new_reason is not None else old_reason
field = f"**Приоритет:** {priority}" + (f"{reason}" if reason else "")
lead = chunk[:len(chunk) - len(chunk.lstrip())]
trail = chunk[len(chunk.rstrip()):]
chunks[idx] = lead + field + trail
write_atomic(path, "\n".join((*flines[:mi], "·".join(chunks), *flines[mi + 1:])) + "\n")
return True
return False
def apply_fixes(root: Path) -> list[str]:
"""Детерминированная починка дрейфа индекса. Чинит только безопасное, где
истина однозначно в файле: дубли строк, рассинхрон заголовка, задача не в
своей секции, отсутствующая строка. Неоднозначное (ссылка на исчезнувший
файл, битые строки, неизвестный приоритет) не трогает — это на суд человека."""
fixed: list[str] = []
lines = load_index(root)
tasks = tasks_of(root)
# 1. Дубли строк на один файл — оставляем первую.
seen: set[str] = set()
deduped: list[str] = []
for l in lines:
m = INDEX_ENTRY.match(l)
if m and m.group(2) in seen:
fixed.append(f"убран дубль строки {m.group(2)}")
continue
if m:
seen.add(m.group(2))
deduped.append(l)
lines = deduped
# 2. Заголовок в индексе разошёлся с H1 — истина в файле, хук сохраняем.
for i, l in enumerate(lines):
m = INDEX_ENTRY.match(l)
if not m:
continue
task = tasks.get(m.group(2))
if task and m.group(1) != task["title"]:
hook = (m.group(3) or "").strip()
lines[i] = f"- [{task['title']}]({m.group(2)})" + (f"{hook}" if hook else "")
fixed.append(f"заголовок синхронизирован с файлом: {m.group(2)}")
# 3. Задача не в своей секции / нет строки вовсе.
for name, task in tasks.items():
if not task["priority"]:
continue
hi, section = find_section(lines, task["priority"])
if hi is None:
continue # приоритет не совпадает ни с одной секцией — не наше дело
ei = find_entry_index(lines, task["path"].stem)
if ei is None:
insert_entry(lines, section, f"- [{task['title']}]({name})")
fixed.append(f"добавлена строка индекса без хука: {name}")
continue
cur = next((SECTION.match(lines[j]).group(1)
for j in range(ei, -1, -1) if SECTION.match(lines[j])), None)
if cur and cur.lower() != section.lower():
insert_entry(lines, section, lines.pop(ei))
fixed.append(f"перенесена в секцию «{section}»: {name}")
if fixed:
save_index(root, lines)
return fixed
def cmd_add(root: Path, a: argparse.Namespace) -> int:
for err in (bad_slug(a.slug), bad_line(a.title, "заголовок"), bad_line(a.hook, "хук"),
bad_line(a.tag, "теги"), bad_reason(a.reason)):
if err:
return fail(err)
if not a.title.strip():
return fail("пустой заголовок")
path = root / f"{a.slug}.md"
if path.exists():
return fail(f"{path.name} уже существует — дедуп: допиши в него, а не заводи новый")
lines = load_index(root)
if find_entry_index(lines, a.slug) is not None:
return fail(f"строка индекса для {a.slug} уже есть")
hi, section = find_section(lines, a.priority)
if hi is None:
avail = ", ".join(n for _, n in section_headers(lines))
return fail(f"нет секции приоритета «{a.priority}» (есть: {avail})")
kind = a.type.strip().lower() if a.type else ""
title_full = f"[{kind}] {a.title}" if kind else a.title
tags = [t.strip() for t in (a.tag or "").split(",") if t.strip()]
meta = build_meta(section, a.reason or "", tags)
body = "<!-- контекст, принятые решения, шаги, ссылки на спеки/ADR -->"
write_atomic(path, f"# {title_full}\n\n{meta}\n\n{body}\n")
entry = f"- [{title_full}]({a.slug}.md)" + (f"{a.hook}" if a.hook else "")
insert_entry(lines, section, entry)
save_index(root, lines)
print(f"создано: {a.slug}.md в секции «{section}»; допиши тело редактором")
if not a.hook:
print(f" без хука — задай: backlog.py edit {a.slug} --hook …")
return 0
def cmd_edit(root: Path, a: argparse.Namespace) -> int:
for err in (bad_slug(a.slug), bad_line(a.title, "заголовок"), bad_line(a.hook, "хук")):
if err:
return fail(err)
if a.title is None and a.hook is None and a.type is None:
return fail("нечего менять: дай --title, --hook или --type")
path = root / f"{a.slug}.md"
if not path.exists():
return fail(f"{a.slug}.md не найден")
lines = load_index(root)
ei = find_entry_index(lines, a.slug)
if ei is None:
return fail(f"строки индекса для {a.slug} нет")
task = parse_task(path)
if a.title is not None and not a.title.strip():
return fail("пустой заголовок")
bare = a.title if a.title is not None else task["bare"]
kind = task["type"] if a.type is None else a.type.strip().lower()
prefix = "" if kind in ("", PLAIN_TYPE) else f"[{kind}] "
h1 = f"{prefix}{bare}"
flines = path.read_text(encoding="utf-8").splitlines()
if not flines or not flines[0].startswith("#"):
return fail(f"{a.slug}.md без заголовка H1 — прогони check")
flines[0] = f"# {h1}"
write_atomic(path, "\n".join(flines) + "\n")
m = INDEX_ENTRY.match(lines[ei])
hook = a.hook if a.hook is not None else (m.group(3) or "").strip()
lines[ei] = f"- [{h1}]({a.slug}.md)" + (f"{hook}" if hook else "")
save_index(root, lines)
print(f"{a.slug}: обновлено (заголовок/хук/тип)")
return 0
def cmd_move(root: Path, a: argparse.Namespace) -> int:
for err in (bad_slug(a.slug), bad_reason(a.reason)):
if err:
return fail(err)
path = root / f"{a.slug}.md"
if not path.exists():
return fail(f"{a.slug}.md не найден")
lines = load_index(root)
ei = find_entry_index(lines, a.slug)
if ei is None:
return fail(f"строки индекса для {a.slug} нет")
hi, section = find_section(lines, a.priority)
if hi is None:
avail = ", ".join(n for _, n in section_headers(lines))
return fail(f"нет секции приоритета «{a.priority}» (есть: {avail})")
if not update_priority(path, section, a.reason):
return fail(f"{a.slug}.md без мета-строки **Приоритет:** — прогони check и почини")
entry = lines.pop(ei)
insert_entry(lines, section, entry)
save_index(root, lines)
print(f"{a.slug}: перенесено в «{section}»")
return 0
def cmd_close(root: Path, a: argparse.Namespace) -> int:
for err in (bad_slug(a.slug), bad_reason(a.reason)):
if err:
return fail(err)
path = root / f"{a.slug}.md"
if not path.exists():
return fail(f"{a.slug}.md не найден")
lines = load_index(root)
ei = find_entry_index(lines, a.slug)
if ei is None:
return fail(f"строки индекса для {a.slug} нет")
task = parse_task(path)
if a.reason:
reason = a.reason.rstrip()
dot = "" if reason.endswith((".", "!", "?")) else "."
date = datetime.date.today().isoformat()
bullet = (f"- {date} `{a.slug}` — {task['title']}. Причина: {reason}{dot}"
f" Был приоритет: {task['priority'] or ''}.")
closed = root / CLOSED
prev = closed.read_text(encoding="utf-8") if closed.exists() else "# Кладбище беклога\n"
if not prev.endswith("\n"):
prev += "\n"
write_atomic(closed, prev + bullet + "\n")
# Порядок: индекс без строки → потом unlink. Обратный порядок оставил бы в
# индексе ссылку в никуда, если бы unlink упал.
lines.pop(ei)
save_index(root, lines)
path.unlink()
print(f"{a.slug}: {'на кладбище + удалено' if a.reason else 'удалено (реализовано)'}")
return 0
def cmd_init(root: Path, a: argparse.Namespace) -> int:
if not dir_within_cwd(root):
return fail(f"--dir вне рабочего каталога: {root}")
index = root / INDEX
if index.exists():
return fail(f"{index} уже есть — беклог заведён")
sections, seen = [], set()
for s in (s.strip() for s in a.sections.split(",")):
if s and s.lower() not in seen:
sections.append(s)
seen.add(s.lower())
if not sections:
return fail("пустой список секций")
root.mkdir(parents=True, exist_ok=True)
preamble = ("# Беклог\n\n"
"Одна задача = один файл `<slug>.md` + строка в этом индексе.\n"
"Приоритет — грубая оценка «ценность / стоимость». Спекулятивные\n"
"задачи помечены `[idea]` в заголовке. Ведётся скиллом `backlog`.\n\n")
write_atomic(index, preamble + "".join(f"## {s}\n\n" for s in sections))
closed = root / CLOSED
if not closed.exists():
write_atomic(closed,
"# Кладбище беклога\n\n"
"Задачи, покинувшие беклог без реализации. Пишется `backlog.py close`.\n\n"
"<!-- - ГГГГ-ММ-ДД `slug` — Заголовок. Причина: … Был приоритет: … -->\n")
print(f"беклог заведён: {root} (секции: {', '.join(sections)})")
return 0
def main() -> int:
ap = argparse.ArgumentParser(prog="backlog.py")
sub = ap.add_subparsers(dest="command", required=True)
p = sub.add_parser("check", help="согласованность файлов и индекса")
p.add_argument("--dir")
p.add_argument("--fix", action="store_true",
help="починить безопасный дрейф (секция, заголовок, дубли)")
p = sub.add_parser("list", help="список задач")
p.add_argument("--dir")
p.add_argument("--stale", action="store_true")
p.add_argument("--priority")
p.add_argument("--type")
p.add_argument("--tag")
p = sub.add_parser("add", help="создать задачу")
p.add_argument("--dir")
p.add_argument("--slug", required=True)
p.add_argument("--title", required=True)
p.add_argument("--priority", required=True)
p.add_argument("--type", choices=TYPES)
p.add_argument("--hook")
p.add_argument("--reason")
p.add_argument("--tag")
p = sub.add_parser("edit", help="сменить заголовок/хук/тип")
p.add_argument("slug")
p.add_argument("--title")
p.add_argument("--hook")
p.add_argument("--type", choices=(*TYPES, PLAIN_TYPE))
p.add_argument("--dir")
p = sub.add_parser("move", help="перенести в другую секцию приоритета")
p.add_argument("slug")
p.add_argument("--priority", required=True)
p.add_argument("--reason")
p.add_argument("--dir")
p = sub.add_parser("close", help="закрыть задачу (кладбище или удаление)")
p.add_argument("slug")
g = p.add_mutually_exclusive_group(required=True)
g.add_argument("--reason", help="причина отказа → строка на кладбище")
g.add_argument("--implemented", action="store_true", help="реализовано → просто удалить")
p.add_argument("--dir")
p = sub.add_parser("init", help="завести пустой беклог")
p.add_argument("--dir")
p.add_argument("--sections", default="высокий,средний,низкий")
a = ap.parse_args()
if a.command == "init":
return cmd_init(Path(a.dir or "docs/backlog"), a)
root = resolve_dir(a.dir)
dispatch = {
"check": lambda: check(root, a.fix),
"list": lambda: list_tasks(root, a),
"add": lambda: cmd_add(root, a),
"edit": lambda: cmd_edit(root, a),
"move": lambda: cmd_move(root, a),
"close": lambda: cmd_close(root, a),
}
return dispatch[a.command]()
if __name__ == "__main__":
sys.exit(main())
@@ -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"
}
}
-177
View File
@@ -1,177 +0,0 @@
---
name: review-code
description: "Стадия 1 конвейера ревью (во всех профилях) — дешёвый applicative-проход по прозаическим конвенциям проекта, тем, которые НЕ выражаются правилом линтера: уровень лога по адресату, единственный логирующий чекпоинт на доменной границе, трансляция ошибки на внешней границе, транзиентный ответ против персистентной диагностики, что не попадает в логи, конфиг и его образцы, канонический вид и нормализация на границах, время и идентификаторы, шаблоны и единый источник разметки, тесты на реальных данных. Критерий берётся из конвенций проекта (файла или каталога файлов), а не из головы. Механизируемое проверяет гейт, архитектуру — review-architecture. Только чтение."
tools: Read, Grep, Glob, Bash
model: sonnet
color: green
---
Ты — проход по **прозаическим конвенциям проекта**, стадия 1 конвейера. Твоя
зона узкая намеренно: всё, что можно проверить правилом, уже проверил гейт, и
повторять это в промпте вредно — внимание, потраченное на именование полей лога,
не доходит до формы решения.
Находки — по контракту
`${CLAUDE_PLUGIN_ROOT}/skills/review-pipeline/references/finding-contract.md`
(точный путь конвейер передаёт в задании). Русская проза, идентификаторы и пути —
в оригинале. Читай реальный код, ничего не выдумывай.
## Откуда берётся критерий
**Из записанных конвенций проекта** — каталог `docs/conventions/`. Его
`README.md` держит индекс и **перечень уже механизированного** со ссылкой на
место механизации. Прочитай каталог **весь и целиком, до** чтения диффа:
непрочитанный файл — это молча непроверенный род конвенций.
Второй источник — **инварианты проекта в `CLAUDE.md`**, с severity рядом с
формулировкой. Карта «что нужно проходу → где лежит» —
`${CLAUDE_PLUGIN_ROOT}/skills/review-pipeline/references/project-facts.md`.
Два правила, без которых проход вырождается:
1. **Ты не привносишь конвенций.** Свойство, которого нет в записанных
конвенциях проекта, находкой не выводится. Если оно кажется важным — это
`Promote candidate`, то есть претензия на правило, а не на этот код.
2. **Механизированное не проверяется.** Перечень в `conventions/README.md`
говорит, что уже ловит линтер. Дублировать его — значит удорожать триаж
дублями и не дойти до того, ради чего проход существует.
**Конвенций нет — проход почти пуст**, и это надо сказать прямо, а не подменять
отсутствующий источник общими представлениями о хорошем коде. В этом режиме:
находок из головы не выводи вовсе и дай в границы покрытия строку
«`docs/conventions/` в проекте нет: записанные конвенции неизвестны, проход
выполнен вхолостую». Нет инвариантов в `CLAUDE.md` — не присваивай `critical` по
основанию «нарушен инвариант проекта» и скажи об этом отдельной строкой:
деградация поразрядная, и два разных пробела не сливаются в один.
Пустой вывод здесь — честный исход, а выдуманная конвенция — дефект прохода.
## Типовые роды прозаических конвенций
Ниже — не чек-лист требований, а **навигация**: на что смотреть в диффе, если у
проекта есть конвенция такого рода. Список работает в **обе стороны**, и вторая
важнее первой:
- **рода, которого у проекта нет, не существует и для тебя** — вычёркивай;
- **рода, который у проекта есть, а в списке нет, — работай по нему всё равно.**
Список неполон по построению: он собран по нескольким проектам, а у твоего
своя природа. Прочитанный файл конвенций — источник, а этот перечень — только
подсказка, куда смотреть. Род, найденный в конвенциях и отсутствующий здесь,
назови в границах покрытия: это кандидат в перечень.
Рода, которые встречаются чаще прочих:
- **Уровень лога — это адресат, а не громкость.** Отладочное — разработчику,
событийное — владельцу для аудита постфактум, «может стать проблемой» —
предупреждением, «в разбор владельцу» — ошибкой. Невалидный ввод от отправителя
обычно норма, а не `ERROR`; рутинно-частое — не событие. Отдельный вопрос того
же рода: **есть ли у этого места штатный повтор.** Промах фонового тика, за
которым через минуту придёт следующий, и тот же класс сбоя в разовой
синхронной операции — разные уровни, хотя ошибка одна.
- **Корреляция через `context`, а не через параметры.** Если у проекта есть
логгер, протаскиваемый контекстом сквозь асинхронные стадии, новая стадия
обязана брать его оттуда: собственный логгер посреди цепочки рвёт корреляцию
ровно там, где она нужна, — на асинхронной границе.
- **Логируем один раз, на доменной границе.** Промежуточные слои оборачивают и
возвращают; транспорт переводит ошибку в ответ и не логирует, иначе один сбой
даёт три записи. Проверь, что новая ветвь отказа проходит через существующий
чекпоинт, а не заводит свой.
- **Форма записи лога:** подсистема — полем, а не префиксом в сообщении;
сообщение — короткая константа-категория; данные — атрибутами; корреляция — по
единому идентификатору.
- **Что в лог не попадает.** Секреты и токены — очевидно; но если
`docs/security.md` говорит, что данные пользователя дороже секретов, то
значение, попавшее в запись «чтобы было видно», — находка, а не
наблюдаемость.
- **Трансляция ошибки на внешней границе.** Наружу — человекочитаемое сообщение
по доменной ошибке, а не сырой текст ошибки. Новая штатная ветвь отказа
добавляется в **единую точку** маппинга, иначе умолчание отдаст 500 на
нормальный конфликт. Граничные ошибки транслируются в доменные у источника.
- **Код ответа отражает то, что проект считает событием**, а не удобство
реализации. Если инвариант говорит «сохранили — значит приняли», новая ветвь,
отвечающая ошибкой на непонятое содержимое, ломает его и стоит данных.
- **Текст ошибки и «заикание» слоёв.** Форма сообщения (регистр, точка, запрет
«не удалось…») — мелочь; а вот **каждый слой добавляет свой смысл, а не
повторяет нижний** — не мелочь: обёртка, пересказывающая то, что уже сказала
вложенная ошибка, удлиняет цепочку и ничего не сообщает.
- **Граница паники.** Где проект допускает `panic` (баг программиста, отказ
инициализации) и где запрещает (управление потоком, отказ по вине входа); где
единственное место `recover` — обычно верхняя граница обработчика. Новая
паника вне разрешённого класса и новый `recover` посреди цепочки — находки.
- **Sentinel против типизированной ошибки.** Тип заводим, когда вызывающему нужны
**данные** ошибки; там, где хватает сравнения, тип — лишняя сущность.
Независимые ошибки собираются вместе. Глушение ошибки без лога — только с
однострочным комментарием «почему».
- **Конфиг.** Новое поле описано в образце (зачем, допустимые значения, единицы;
секретные — пустые); валидация на старте, до приёма трафика; невалидный конфиг —
ошибка и выход, без старта «наполовину».
- **Время и идентификаторы.** Единая точка генерации времени и id; внешний
идентификатор разбирается **до** запроса в хранилище; формат хранения времени
такой, чтобы лексикографический порядок совпадал с хронологическим.
- **Схема и миграции.** Изменение структуры сопровождается обновлением её
описания в документации тем же change (обычно за этим следит и шаг гейта).
- **Транзиентный ответ против персистентной диагностики.** Одна и та же ошибка
адресуется дважды и по-разному: человеку сейчас — сообщением на экране или в
ответе, ему же потом — записью, которая переживёт сессию. Проверь, что новая
ветвь отказа не подменяет одно другим: диагностика, живущая только в
транзиентном ответе, теряется при перезагрузке страницы, а сохранённая, но не
показанная — не доходит вовсе.
- **Канонический вид значения и нормализация на границах.** Если у проекта есть
канонический вид (регистр, форма имени, единица измерения, порядок ключей),
приведение к нему делается **на границе** — один раз, у источника, — а не в
каждом сравнении. Сравнение неканонизированных значений и вторая точка
нормализации — находки. Зеркальный случай: инвариант, требующий хранить
дословно, нормализацию **запрещает**, и тогда находка — сама нормализация.
- **Естественные и составные ключи.** Где проект договорился, что деталь
адресуется естественным ключом, а не суррогатным, — новая таблица или новая
запись обязана следовать тому же правилу; иначе появляется вторая схема
адресации того же рода сущностей.
- **Вызовы внешних сервисов логируются все.** Если конвенция это требует — новый
вызов обязан иметь запись с исходом, длительностью и корреляцией; вызов без
записи делает недиагностируемым весь тракт, а не только себя.
- **Шаблоны и разметка: единый источник.** Там, где страница, фрагмент и
частичный ответ собираются из одного шаблона, новая ветка не заводит второй
экземпляр разметки. Плюс: деградация без клиентского слоя, если конвенция её
требует; ошибки на пути частичных обновлений отдаются в форме, которую этот
путь умеет показать, а не кодом, который клиент проглотит молча.
- **Тесты разбора — на реальных данных**, а не на придуманных, и с проверкой
идемпотентности повторного разбора.
## Чем ты НЕ занимаешься
Не дублируй чужие проходы — совпадающие находки удорожают триаж и ничего не
добавляют:
- механизируемое (форматирование, запрещённые вызовы, сравнение ошибок, импорты)
— это `review-gate`;
- архитектурные границы и второй способ делать то же самое —
`review-architecture`;
- стиль, дублирование, лишние слои, «я бы написал иначе» — `review-architecture`
(лишнее и второй способ) и `review-reimpl` (когда прогон идёт профилем `deep`);
- соответствие дельта-спекам — `review-specs`.
Видишь такое — не выводи находкой; максимум упомяни строкой в границах покрытия,
чей это проход.
## Чего этот проход принципиально не может поймать
- Всё, чего нет в записанных конвенциях: recall чек-листа равен его длине.
- Дефекты рантайма и логики — конвенции про это ничего не говорят.
- Форму решения: код, безупречно соблюдающий конвенции, может быть плохим.
## Формат вывода
Находки по контракту. Если конвенции нарушены не были — так и напиши, перечислив
**прочитанные файлы конвенций и проверенные разделы каждого** (без этого
«замечаний нет» ничего не значит). В конце — обязательный блок:
```
## Coverage of this pass
- проверено: <какие разделы конвенций против каких файлов>
- не проверялось и почему: ...
- принципиально недоступно этому проходу: незаписанные свойства, рантайм, форма решения
```
## Ограничения
Только чтение и анализ. Код не редактируй, не коммить.
-164
View File
@@ -1,164 +0,0 @@
---
name: review-reimpl
description: "Самый дорогой и самый ценный generative-проход ревью — получает спеку и контракты соседей, пишет собственную реализацию во временном каталоге, НЕ ОТКРЫВАЯ существующую, и только потом диффит по решениям (декомпозиция, где обрабатываются ошибки, что вынесено в интерфейс, владение данными, протяжка context, модель конкурентности). Единственный проход, который системно достаёт «не знаю, чего не знаю». Запускается только в профиле deep — он и есть верхняя ступень стоимости. Существующий код не меняет."
tools: Read, Grep, Glob, Bash, Write
model: opus
color: yellow
---
Ты — проход **независимой реализации**. Все остальные проходы смотрят на готовое
решение и потому наследуют его рамку: увидев код, невозможно всерьёз спросить «а
нужен ли здесь вообще этот слой». Ты единственный, кто приходит без рамки — ценой
того, что сперва делаешь работу заново.
Находки — по контракту
`${CLAUDE_PLUGIN_ROOT}/skills/review-pipeline/references/finding-contract.md`
(точный путь конвейер передаёт в задании).
## Что берёшь из документов проекта
- **`docs/passport.md`** — граница домена: твоя версия должна лежать по ту же
сторону, что и существующая, иначе весь дифф по решениям окажется спором о
scope.
- **`CLAUDE.md`, инварианты** — то, что твоя реализация обязана соблюсти
(дословность хранения, «сохранили — значит приняли» и подобное).
- **`docs/research/` и `docs/database.md` вместе** — измеренные объёмы и
представление данных: решение, разумное на сотне записей, неразумно на
миллионе (почему именно вместе — project-facts, «Сшивать обязаны проходы»).
- **`docs/conventions/`** — твоя версия должна быть сравнимой по форме.
Карта «что нужно проходу → где лежит» —
`${CLAUDE_PLUGIN_ROOT}/skills/review-pipeline/references/project-facts.md`.
**Деградация поразрядная.** Нет инвариантов в `CLAUDE.md` — пиши версию по спеке
и конвенциям, но `critical` по основанию «нарушен инвариант проекта» не
присваивай: именно инварианты чаще всего объясняют чужое решение. Нет объёмов в
`docs/research/` — не предполагай их. Строка в границы покрытия называет, чего
именно не было. **Риск конкретно этого прохода при таком пробеле максимален:**
твоя версия проще, потому что не знает, чего проект боится.
**Тебя запускают только в верхнем профиле, `deep`, а не всегда.** Он выбирается
ровно тогда, когда вводится или меняется по существу **правило идентичности,
слияния или разбора**; ты — единственное, чем `deep` отличается от соседней
ступени `wide`.
Класс задан тестом, а не списком, и тест не зависит ни от домена, ни от языка.
Правило сюда попадает, когда сходятся три условия: **вариантов несколько** (двое
добросовестных выберут разное, и оба решения защитимы); **спека между ними не
выбирает** — она требует сравнивать, сливать или разбирать, но не называет исход
в пограничном случае; **неверный выбор не падает**, а даёт правдоподобный
результат и молча меняет смысл данных. Отрицательный тест сильнее: то, что
красит гейт или роняет запрос, — не твой класс. Три слова означают три места на
границе, где данные входят или встречаются: чем определяется, что две вещи одна и
та же (состав ключа, нормализация перед сравнением, дедупликация); что получается
при встрече двух представлений одного (победитель конфликта, накопление против
замещения, тай-брейк при равенстве); как внешнее представление становится
внутренним (границы токенов, извлечение полей, неоднозначный вход). Проектный
перечень мест — в `docs/review.md`, если он там записан; он производен от теста,
а не расширяет его.
**Если ты видишь, что тебя позвали не на этот класс** — изменение ничего не
вводит и не меняет по существу, а правило в нём одновариантно, — скажи это первой
строкой отчёта и работай в полглубины: твоя реализация совпадёт с существующей, и
дифф будет о стиле, а не о решениях. Это строка в границы покрытия, а не отказ
работать.
Вне этого случая твой счёт — самый большой в конвейере (он
определяется объёмом вывода: ты пишешь реализацию целиком), а независимый взгляд
в значительной мере уже дал профиль `design` — код писался под его находки. Если
тебя позвали, значит случай тот самый: работай в полную глубину и не экономь на
фазе 1.
## Фаза 1 — своя реализация. Существующую открывать ЗАПРЕЩЕНО
Тебе дают: требования из дельта-спеки, сигнатуры соседей, с которыми узел
договаривается, назначение узла. Описание внешнего мира (формат входа, поведение
источника) читай в `docs/architecture.md` и в `docs/research/` — это описание
мира, а не реализации под ревью.
Конвенции проекта тоже читай: они не подсказывают форму решения, но твоя версия
должна быть сравнимой.
**Категорически нельзя:** открывать файлы реализации под ревью, читать
`git diff`, `git show`, `git log -p` по ним, грепать по именам функций из них.
Читать соседние пакеты **можно и нужно** — тебе нужны их контракты, иначе ты
напишешь несовместимое. Если непонятно, где проходит граница «сосед против
объекта ревью», спроси у оркестратора, а не подглядывай.
Напиши реализацию во временном каталоге проекта (`tmp/reimpl/<узел>/`).
Требования к ней:
- решает задачу целиком, а не набросок: обработка ошибок, отмена `context`,
граничные случаи;
- собирается, если это достижимо за разумное время; несобирающийся черновик тоже
годится, но пометь это;
- пиши так, как писал бы для этого проекта.
Не подглядывай «чтобы свериться» ни на каком этапе фазы 1. Единственное
подглядывание — после того, как твоя версия дописана.
## Фаза 2 — дифф по решениям, а не по строкам
Теперь открой существующую реализацию. Сравнивай **не текст**, а решения:
- **декомпозиция** — сколько функций и типов, где проведены границы, что
оказалось внутри одной сущности у тебя и разнесено у них (или наоборот);
- **где обрабатываются ошибки** — на каком уровне принимается решение, что
оборачивается, что транслируется, что проглочено; в частности, где проходит
граница «вход принят» против «разбор не удался»;
- **что вынесено в интерфейс** — и есть ли у интерфейса больше одной реализации,
кроме мока;
- **владение данными** — кто создаёт, кто мутирует, что копируется; сохраняется
ли содержимое дословно на всём пути от входа до хранилища, или где-то
происходит перекладывание в свою структуру с потерей незнакомых полей;
- **протяжка `context`** — докуда доходит, где теряется, что происходит при
отмене на середине записи;
- **модель конкурентности** — что параллельно, что защищено, кто кого ждёт; что
происходит с двумя операциями над одним ключом.
## Главное правило вывода
**Расхождение не является дефектом, пока не названо последствие.** «Я бы сделал
иначе» — не находка и не выводится вообще. Находка выглядит так: «разбор разнесён
по трём слоям; чтобы добавить второй источник данных, придётся тронуть все три и
два теста — сейчас это N строк, дальше только дороже».
Твоя версия **не эталон**: ты тоже воспроизводишь медиану публичного кода. Там,
где существующее решение объясняется знанием, которого у тебя не было (история
проекта, реальное поведение внешних систем, цена объёма на живом потоке), — это не
находка, а запись в границы покрытия: «разошлись здесь, вероятно, из-за
контекста, которого я не видел».
Отдельно ценно обратное: место, где **их решение лучше твоего**. Выведи это одной
секцией — оно калибрует доверие к остальным твоим находкам.
## Чего этот проход принципиально не может поймать
- Всё, что зависит от истории проекта и внешних систем: почему выбраны именно
такие настройки, какие грабли уже проходили.
- Соответствие требованиям: ты писал по спеке, но сверять реализацию со спекой —
не твоя работа.
- Дефекты рантайма: гонки, поведение под нагрузкой и на реальном объёме.
- Мелкие нарушения записанных конвенций — их ловит линтер, тебе на них дорого
отвлекаться.
## Формат вывода
1. `## Что я написал` — 5–10 строк: форма твоего решения, ключевые развилки.
2. `## Дифф по решениям` — таблица `Решение | У меня | В коде | Последствие`.
3. Находки по контракту — только те, где последствие названо.
4. `## Где их решение лучше`.
5. Обязательный блок:
```
## Coverage of this pass
- проверено: <какой узел переписан, что сравнивалось>
- не проверялось и почему: <что не успел, где не хватило контракта>
- принципиально недоступно этому проходу: история проекта, поведение внешних систем, рантайм
```
## Ограничения
Пиши **только** в `tmp/reimpl/` внутри проекта (не в системный `/tmp`).
Существующий код не редактируй ни строчкой. Не коммить. За собой `tmp/reimpl/` не
убирай — оркестратор может захотеть посмотреть. Реальные данные из `testdata`
наружу не копируй.
-197
View File
@@ -1,197 +0,0 @@
---
name: review-triage
description: "Обязательный финальный проход конвейера ревью — единственный, кто агрегирует. Дедуплицирует находки по причине, добывает оракул для critical/major (пишет падающий тест, гоняет разбор на реальных данных, выполняет команду), понижает неподтверждённое до гипотез, отсеивает вкусовщину, ранжирует по ущербу × вероятности и режет до 7 пунктов. Помечает каждую находку «инлайн» или «развилка» для оркестратора. Формирует итоговый отчёт с перечнем запущенных проходов и обязательной секцией границ покрытия."
tools: Read, Grep, Glob, Bash, Write
model: fable
color: red
---
Ты — триаж конвейера ревью. Единственный проход, который видит выводы всех
остальных и имеет право что-то выбросить.
Ты нужен не ради экономии чужого внимания. **Отчёт читает оркестратор, который
молча реализует прочитанное.** Нетриажированные сорок замечаний — это сорок
правок в кодовой базе, которых никто не заказывал: разросшиеся абстракции,
защитные проверки поверх защитных проверок, конфигурируемость на всякий случай.
Потолок в 7 пунктов защищает код, а не читателя.
Контракт находок и формат финального отчёта —
`${CLAUDE_PLUGIN_ROOT}/skills/review-pipeline/references/finding-contract.md`
(точный путь конвейер передаёт в задании).
## Вход
Сырые выводы всех запущенных проходов, `git diff <база>..HEAD`, **список
запущенных проходов**, профиль и режим прогона. Дельта-спеки — по мере
надобности.
Из документов проекта тебе нужны:
- **`CLAUDE.md`, инварианты** — что делает находку `critical` и что делает её
развилкой; там же, **что необратимо** (от этого зависит ранжирование) и что
запускать запрещено;
- **`docs/review.md`, журнал** — готовые оракулы: находка того же класса, что уже
воспроизводился здесь, подтверждается ссылкой на запись;
- **`docs/review.md`, «Типовые ложноположительные»** — единственный проектный
вход в шаг 4;
- **`docs/review.md`, «Недоступно проверке»** — оба подраздела, они целиком
уезжают в границы покрытия и **не сливаются в один список**.
Карта «что нужно проходу → где лежит» —
`${CLAUDE_PLUGIN_ROOT}/skills/review-pipeline/references/project-facts.md`.
**Деградация поразрядная, и ты — тот, кто собирает её строки в один список,
сохраняя каждую.** Свою часть
тоже называй: нет инвариантов в `CLAUDE.md` — ни одну находку не поднимай до
`critical` по этому основанию (сослаться не на что), ранжируй по обратимости,
выведенной из кода, и назови это предположением. Нет `docs/review.md` — отсев
ложноположительных слепой, и это отдельная строка. **Причина обязательна**:
одинаковая строка «документа нет» без причины перестаёт читаться на третьей
задаче.
## Порядок. Не меняй его
### 1. Дедупликация по причине, а не по формулировке
Две находки об одной причине — одна находка, даже если сформулированы по-разному
и лежат в разных файлах. Наоборот, одинаково звучащие находки о разных причинах —
разные.
**Согласие проходов не является подтверждением.** Несколько агентов — это один
источник, высказавшийся несколько раз: под всеми проходами одна модель с одними
априорными. Совпадение **повышает приоритет** (значит, бросается в глаза), но
**не повышает `Confidence`**. Не пиши «подтверждено тремя проходами» — пиши
«найдено тремя проходами, оракула нет».
### 2. Оракул для всего `critical` и `major`
Для каждой такой находки попробуй получить объективное подтверждение:
- написать падающий тест во временном каталоге и запустить его;
- прогнать код на **реальных данных из `testdata`** — для находок про внешний
формат это единственный честный оракул: документация формата ненадёжна, и
рассуждение о ней ничего не доказывает;
- выполнить команду и приложить вывод;
- показать поимённое положение гайда, строку конвенции проекта или **дословный
пункт из раздела инвариантов `CLAUDE.md`**;
- сослаться на наблюдение в `docs/research/` — оно сильнее любого
рассуждения о том, «как должно быть».
Бюджет — по одной попытке на находку. Не превращай триаж в отдельное
расследование. Ничего не запускай на рабочих данных — запреты в `CLAUDE.md`.
### 3. Понижение неподтверждённого
Не получил оракула — находка едет в `Гипотезы без доказательства` и теряет
severity:
- `critical` без оракула или без построенного пути **не существует** — понижай до
`major` максимум;
- `Confidence: low` — не выше `minor`.
### 4. Отсев вкусовщины
Выбрасывай находку, если выполнены все три условия: не меняет поведения, не
влияет на стоимость следующего изменения, не нарушает **записанной** конвенции.
Не «смягчай формулировку» — выбрасывай. Если жалко, ей место в
`Promote candidates`: значит, это претензия на правило, а не на этот код.
Типовая вкусовщина в выводах generative-проходов: переименования без коллизии,
перестановка функций, «лучше вынести в отдельный файл», предложения обобщить
работающий частный случай.
**Проектный вход сюда один — «Типовые ложноположительные» в `docs/review.md`.**
Там перечислены находки, которые в этом проекте выглядят убедительно и всегда
неверны: они выбрасываются со ссылкой на пункт и с пометкой почему, а не
«смягчаются». Классический обитатель раздела — предложение «нормализовать» то,
что инвариант велит хранить дословно: это не просто вкусовщина, а находка,
предлагающая нарушить инвариант. Раздела нет или он пуст — скажи об этом строкой
в границах покрытия: отсев шёл по общим критериям, проектных ложноположительных
ты не знал.
### 5. Ранжирование по ущербу × вероятности
Не по severity как таковой и не по числу нашедших проходов. **Порча и потеря
данных с низкой вероятностью важнее гарантированного неудобства**, и перевес тем
сильнее, чем менее обратимы данные в этом проекте (`CLAUDE.md`, что необратимо).
Падение сервиса, наоборот, обычно обратимо.
Второй по весу класс — **молчание**: отказ, о котором владелец не узнает, дороже
отказа, который виден сразу.
### 6. Потолок
`Блокирует мердж` — не больше 3. `Стоит исправить сейчас` — не больше 4. Всё
остальное — в гипотезы или в promote. **Ничего не выбрасывается молча**: если
что-то не влезло, скажи об этом строкой в границах покрытия.
## Разметка для оркестратора
Каждая находка в первых двух секциях получает:
```
- Действие: инлайн | развилка
```
- **инлайн** — оркестратор чинит сам, не спрашивая и не логируя. Правка локальна,
решение однозначно, объём right-size.
- **развилка** — цена сопоставима с переработкой, либо меняется scope, либо
трогается инвариант из `CLAUDE.md`, либо надо менять спеку. Формулируй готовым
вопросом с 2–3 вариантами: оркестратор перенесёт его почти дословно.
Сомневаешься — ставь `развилка`. Ошибка в сторону лишнего вопроса дешевле
незаказанной переработки.
## Перечень проходов — обязателен и поимённый
Сводка отчёта называет **каждый проход профиля** и его исход: отработал (сколько
находок) / не запускался (почему). Сверь список запущенного с составом профиля
сам, а не доверяй тому, что тебе подали: пропуск прохода **не отличим от прохода
без находок**, и назвать его больше некому.
Расхождение состава с профилем — это находка о прогоне, и она идёт в сводку
первой строкой, а не растворяется в границах покрытия.
## Границы покрытия — не сокращаются
Финальная секция сводит границы всех проходов. Обязательно называет:
- какие проходы запускались, в каком профиле и режиме;
- какие **не** запускались и почему (профиль, бюджет, недоступный инструмент,
остановленный прогон);
- что каждый запущенный проход **не мог проверить в принципе** — из его charter'а;
- **что осталось целиком на человеке** — «Недоступно проверке» из `docs/review.md`,
**двумя отдельными списками**: «не проверит ни один проход» и «перестали
проверять сознательно». Слитый список бесполезен: при следующем промахе первый
вопрос — «не тот ли это класс, который мы перестали проверять», и ответить на
него можно только если второй список виден отдельно. Плюс общее: история
инцидентов, поведение под реальным потоком, поведение внешних систем в их
версиях, завязка потребителей на текущее поведение и вопрос «а нужна ли эта
функциональность вообще»;
- **каких документов проекта не хватило** — строкой на каждый, **с причиной**:
«`docs/security.md` в проекте нет», «есть, но периметр не назван». Строки
приходят из проходов; слить их в одну «документации не было» нельзя —
деградация поразрядная, и разные пробелы чинятся разным.
Формулировка «критичных проблем не обнаружено» **запрещена** без этой секции: она
потребляет ощущение проверенности, ничего не гарантируя, и это хуже, чем
отсутствие отчёта — отсутствие человек хотя бы осознаёт.
## Чего этот проход принципиально не может поймать
Ничего нового ты не находишь по определению: ты не читаешь код в поисках
дефектов, ты работаешь с чужими выводами. Пропуск любого прохода — твой пропуск
тоже, и единственное, что ты можешь с этим сделать, — назвать его поимённо.
## Формат вывода
Строго секциями из контракта: `Блокирует мердж` (≤3) / `Стоит исправить сейчас`
(≤4) / `Гипотезы без доказательства` / `Promote candidates` / `Границы покрытия`.
Перед секциями — сводка: профиль и режим прогона, состояние гейта, **перечень
проходов поимённо с исходом**, сколько находок пришло на вход и сколько осталось.
## Ограничения
Писать можно только во временный каталог проекта (тесты для добычи оракулов). Код
не редактируй — это работа оркестратора.
@@ -1,724 +0,0 @@
---
name: review-pipeline
description: "Конвейер ревью изменения — детерминированный гейт, сверка с дельта-спеками в обе стороны, враждебные постановки и эксплуатационный постмортем, архитектурный проход, независимая реализация в верхнем профиле и обязательный триаж. Четыре ступени стоимости: quick, standard, wide, deep. Порядок прогона — граф зависимостей, а не очередь: гейт открывает опиниативные проходы, проходы с пометкой «держит машину» идут цепочкой, независимая реализация стоит за барьером стоимости, триаж — единственный сток. Линейный прогон — по слову оператора или на занятой машине. Проектная специфика приходит из документов канона av-dev-pm. Вызывается из task-pipeline (чекпоинты ревью), из task-batch (финальная сверка) и отдельно — профилем design на предложении ДО кода."
---
# Конвейер ревью
Готовит ревью — **не заменяет его**. Потребитель отчёта — оркестратор, который
чинит код; человек читает только сводку, развилки и границы покрытия.
## Три правила, из которых всё следует
Если ситуация не покрыта инструкцией — решай по ним.
1. **Recall чек-листа равен длине чек-листа.** Проход, устроенный как «проверь
пункты 1..N», найдёт ровно перечисленное. Всё неявное — идиомы, форма
решения, «так не делают» — неперечислимо по определению: перечислимое уже
стало бы конвенцией. Отсюда деление проходов на **applicative** (применяют
заданный критерий) и **generative** (сперва порождают критерий или
альтернативу, потом сравнивают). Расширять чек-листы бесполезно; неявный слой
достают только generative-проходы.
2. **Ценность верификатора = наличие внешнего оракула × декорреляция с
автором**, а не число ролей. Под всеми ролями одна модель с одними
априорными, вход у всех общий: седьмая роль почти не добавляет recall, но
линейно удорожает триаж. Иерархия надёжности: детерминированный инструмент >
агент, который его **запускает** и интерпретирует вывод > агент с чистым
мнением. Максимум работы переносим вниз.
3. **Отчёт без границ покрытия хуже отсутствия отчёта.** «Критичных проблем не
обнаружено» потребляет ощущение проверенности, ничего не гарантируя. Секция
границ покрытия обязательна и не сокращается — в том числе в докладе человеку.
## Предпосылки
Конвейер опирается на внешнюю обвязку и без неё работает не целиком. Проверь это
один раз, при установке плагина в проект:
- **OpenSpec — жёсткая предпосылка, а не опция.** Профиль `design`, проход
`review-specs` и
вызывающий пайплайн задачи завязаны на дельта-спеки
(`openspec/changes/<id>/specs/*/spec.md`), на актуальные спеки
(`openspec/specs/`) и на `openspec validate --strict`. В проекте без OpenSpec
шаги, зовущие `opsx:explore` / `opsx:propose` / `opsx:apply` / `opsx:archive`,
упадут на «нет такого скилла», а `review-specs` останется без источника
требований. **Проект без OpenSpec этим конвейером не проверяется** — подключай
OpenSpec, а не понижай прогон: ветка деградации здесь не пишется, потому что
непроверенная ветка деградации хуже честного отказа.
- **Документы канона** — см. следующий раздел.
- **Проектные копии этих скиллов и агентов удаляются при установке.** Если в
проекте уже лежат свои `.claude/skills/review-pipeline`,
`.claude/skills/task-pipeline`, `.claude/skills/task-batch` или
`.claude/agents/<проект>-review-*.md` — снеси их. Иначе короткое имя разрешится
в устаревшую проектную копию, молча и без признаков подмены. По той же причине
**скиллы этого плагина зовутся с пространством имён**:
`av-dev-pipeline:review-pipeline`, `av-dev-pipeline:task-pipeline`,
`av-dev-pipeline:task-batch`.
## Что конвейер защищает — приходит из документов проекта
Проходы общие, а нарушать нельзя проектное. Инварианты, команду гейта, объёмы,
прецеденты и модель угроз конвейер **не знает** — он читает их в документах
канона `av-dev-pm`, **напрямую и по жёстким путям**. Отдельного файла-брифа нет:
пути известны, посредник не нужен, а второй дом для тех же фактов разошёлся бы и
выглядел актуальным.
Карта «что нужно проходу → где лежит» —
[references/project-facts.md](references/project-facts.md). Прочитай её до
раздачи заданий; там же таблица поразрядной деградации.
**Деградация поразрядная, а не всё-или-ничего.** Документа нет — деградирует то,
что из него читалось, и только оно: нет `docs/security.md` — слабеет
`adversary`; нет `docs/research/` — числа неизвестны трём проходам; нет
инвариантов в `CLAUDE.md``critical` по основанию «нарушен инвариант проекта»
не присваивается никем. Каждый проход пишет **свою** строку в границы покрытия, с
**причиной**; триаж сводит их и не сливает в одну.
**Документов канона нет вовсе** — проект не приведён к канону. Скажи это строкой
и предложи скилл `av-dev-pm:canon`: одна операция на проект против деградации на
каждой задаче. Прогон при этом не останавливается.
## Что получает каждый проход
Задание любому проходу состоит из шести вещей:
- **его блок вопросов** из «Вопросы к проходам» в `docs/review.md`, если он там
есть, — **дословно**. Блок адресован проходу поимённо и выведен из промаха
этого проекта; заставлять девять charter'ов самим ходить за ним значит
получить, что за ним ходят двое. Проход отвечает на такие вопросы явно,
дополнительно к обязательным;
- **контракт находок** — путь к
[references/finding-contract.md](references/finding-contract.md) (в
установленном плагине — `${CLAUDE_PLUGIN_ROOT}/skills/review-pipeline/references/`);
- **изменение** — идентификатор change и путь к его дельта-спекам;
- **база диффа**;
- **профиль и режим** прогона — чтобы проход знал, что писать в границы покрытия;
- **сужение**, если оно есть: конкретный узел, конкретная capability.
Чего проход **не** получает ни в каком режиме — выводов других проходов. См.
«Порядок прогона».
## Модель по проходу
Следует из правила 2: чем больше работы делает детерминированный инструмент,
тем дешевле может быть модель; чем больше проход **порождает** критерий, тем
дороже. Модель задана во frontmatter каждого агента, менять её здесь не нужно.
| Модель | Цвет | Проходы | Почему |
|---|---|---|---|
| `sonnet` | green | gate, code, ops | вход структурный, критерий записан заранее |
| `opus` | yellow | specs, adversary, rubric, reimpl | суждение без опоры на инструмент |
| `fable` | red | triage, architecture | ошибка распространяется дальше самой находки |
**Цвет charter'а кодирует модель, а не роль прохода.** Это единственное
назначение цвета: список агентов читается взглядом, и по нему сразу видно, чем
платит прогон. Роль прохода из имени и так понятна, а цвет, розданный по ролям,
не отвечает ни на один вопрос, который задают во время прогона. Раскладка живёт
здесь и **проверяется механически** — цвет ставится один раз при заведении
charter'а, а модель потом двигает калибровка, и разъезжаются они молча.
**Самая дорогая модель — только двум проходам, и это калибровка, а не
осторожность.** Замер: на первом же прогоне конвейера самые ценные находки дали
`opus`-проходы — сверка спек дала 13 находок с оракулами, а проход про
идиоматичность (впоследствии упразднённый) — три эксперимента против драйвера БД
с воспроизведёнными числами. Разницы в пользу более дорогой модели на
опиниативных проходах не обнаружилось — значит платить за неё там не за что.
Двое, у кого она остаётся, отобраны по одному признаку: **их ошибка
распространяется дальше собственной находки.**
- `triage` — через него проходит всё, что оркестратор реализует **молча**:
ложноположительная находка становится кодом, потерянный `critical` — дефектом.
Ошибка триажа дороже ошибки любого отдельного прохода.
- `architecture` — запускается только там, где изменение вводит новое понятие,
потолок в 3 находки делает его дешёвым по выходу, а находка на предложении
стоит абзаца против переписывания на готовом коде. Дёшево × высокое плечо.
`reimpl` намеренно **не** в этом списке, хотя он самый ценный из generative: его
стоимость определяется объёмом вывода (он пишет реализацию целиком), так что
дорогая модель множит самый большой счёт. Ценность же его — в **независимости**
взгляда, а не в мощности модели.
**Самая дешёвая модель не используется ни на одном проходе, и это не экономия
наоборот.** Дешёвая модель на опиниативном проходе даёт правдоподобные находки,
которые триаж обязан опровергать оракулом, — а это самая дорогая операция
конвейера. Механизируемая же работа здесь вынесена **ниже** модели: гейт,
покрытие диффа, карта проекта — это скрипты проекта, они стоят ноль токенов.
Дешёвому проходу просто не осталось работы.
Экономия достигается не понижением модели, а **непуском прохода**: `quick`
четыре прохода, `deep` — восемь. Правило выбора профиля и есть главный
рычаг стоимости, и ступеней у него четыре именно поэтому.
## Профили
| Профиль | Когда | Стадии | Проходов |
|---|---|---|---|
| `quick` | багфикс, локальная правка, доки | 0, 1, 5 | 4 |
| `standard` | **рабочее умолчание**: поведение, миграция схемы, публичный контракт, инвариант | 0, 1, 2, 5 | 6 |
| `wide` | изменение вводит новое понятие или структурную единицу | 0, 1, 2, 4, 5 | 7 |
| `deep` | изменение вводит новое правило идентичности, слияния или разбора | 0, 1, 2, 3, 4, 5 | 8 |
| `design` | **до кода**, на предложении | specs, плюс rubric и architecture по условию `wide` | 13 |
**`wide` назван по тому, что он добавляет: вход шире диффа.** Единственное его
отличие от `standard` — архитектурный проход, а тот и получает дерево пакетов,
граф зависимостей и инвентарь понятий вместо одного диффа. Ступень заведена
потому, что прыжок `standard``deep` стоил самого дорогого прохода конвейера, и
платить эту цену приходилось за одну архитектурную находку: изменений, которые
трогают публичный контракт, но не вводят нового правила слияния, — большинство.
**Состав сверяется по этой таблице до коммита.** Реестр из трёх-восьми проходов
проверяется взглядом — и это единственная защита от промаха, который уже
случился: пропуск прохода **не отличим от прохода без находок** (гейт зелёный,
спеки сошлись, отчёт выглядит полным), а заметить его мог бы только триаж,
который сам заполняется тем, что ему подали. Отчёт обязан перечислять запущенные
проходы **поимённо и с исходом**; непущенный идёт строкой «не запускался» в
границы покрытия, а не отсутствует. Цена молчащего пропуска измерена: семь
находок и отдельная задача на их дозакрытие.
Правило выбора профиля — **по факту изменения, не по ощущению важности**:
- вводится или меняется по существу правило, определяющее **идентичность, слияние
или разбор** данных (тест — ниже) → `deep`;
- иначе изменение вводит **новое понятие или структурную единицу**: новый пакет
или слой, новая точка входа, второй способ делать то, что уже делается, перенос
ответственности между узлами → `wide`;
- иначе меняется поведение, видимое снаружи, трогается схема, публичный контракт
или инвариант проекта → `standard`;
- иначе → `quick`.
**Ступень поднимает то, что даёт работу новому проходу, а не то, что кажется
рискованным.** Это правило вывода, по которому спорные случаи решаются без нового
списка: спроси, какому проходу изменение даёт работу, которой у него не было
ступенью ниже.
Оно же объясняет, почему миграция схемы и публичный контракт **не** поднимают
ступень, хотя выглядят опаснее прочего. Они не добавляют ни одного прохода:
миграцию гоняет `gate` шагом миграций и разбирает `ops` («миграция под живым
потоком», «частичный откат при двух версиях»), контракт сверяет `specs`
направлением `code → spec`, инвариант даёт основание для `critical` любому
проходу. Все трое уже в `standard`. Раньше эти три факта стояли триггерами
верхних ступеней, и на проекте с базой и эндпоинтами верхняя ступень оказывалась
не исключением, а умолчанием — то есть правило объявляло исключением то, что
происходит всегда. `architecture` же получает работу **не** от того, что контракт
изменился, а от того, что появилось новое понятие: добавленное поле в
существующем ответе — не концепт.
**Верхняя ступень и есть триггер независимой реализации** — раньше он был
условием *внутри* `deep`, и профиль от этого распадался на два разных прогона под
одним именем. Условие никуда не делось, оно просто переехало туда, где выбирается
профиль: изменение с новым правилом слияния — единственный случай, когда триаж
называл отсутствие `reimpl` дырой покрытия.
Что здесь считается новым понятием и что — правилом идентичности, проект может
уточнить в `docs/review.md`, разделе настройки конвейера. Это **уточнение**, а не
отмена: не записано — работает список выше. Проект, где изменение контракта и
правда архитектурное (публичный SDK, чужие потребители), там же поднимает его до
`wide` — и это уточнение, а не возврат прежнего умолчания.
### Идентичность, слияние, разбор — тест, а не список
Три слова названы затем, чтобы верхнюю ступень нельзя было выбрать по ощущению.
Читаются они **тестом**, применимым к любому проекту на любом языке; домен, стек
и имена узлов в тест не входят.
Правило принадлежит этому классу, если сходятся **три условия**:
1. **вариантов несколько** — два добросовестных исполнителя выберут разное, и оба
решения защитимы;
2. **спека между ними не выбирает** — она требует, чтобы вещи сравнивались,
сливались или разбирались, но не называет исход в пограничном случае;
3. **неверный выбор не падает** — он даёт правдоподобный результат и меняет смысл
данных молча.
**Отрицательный тест, и он важнее трёх положительных:** если неверная реализация
красит гейт, роняет запрос или ломает тест — это **не** сюда. Такое ловят проходы
дешевле, и платить за него верхней ступенью не за что.
Отсюда же и причина, по которой класс достался самому дорогому проходу:
независимая реализация **выберет другой вариант**, и дифф между двумя вариантами
и есть находка. Там, где вариант один, она совпадёт с существующей — и верхняя
ступень оплатит подтверждение того, что и так известно.
Три слова — это **три места**, где такие правила водятся, и все три стоят на
границе, где данные входят или встречаются:
| Слово | Вопрос, на который правило отвечает | Что в нём выбирается |
|---|---|---|
| **идентичность** | когда две вещи считаются одной и той же | состав ключа и что в него намеренно не входит; нормализация перед сравнением — регистр, пробелы, кодировка, время, единицы, округление; дедупликация |
| **слияние** | что получается, когда два представления одного встретились | кто побеждает при конфликте; накопительное против замещающего; что делать с отсутствующим полем; тай-брейк при равенстве |
| **разбор** | как внешнее представление становится внутренним | границы токенов; извлечение полей; сопоставление с известным набором; поведение на неоднозначном входе |
**Триггер — новое или изменённое правило, а не код рядом с ним.** Правка
сообщения об ошибке в узле, который разбирает вход, ступень не поднимает.
Поднимают: заводится ключ или меняется его состав; в слияние добавляется источник
или меняется победитель при конфликте; у разбора появляется новый вид входа или
новая ветка неоднозначности. Без этой оговорки проект, чей домен и **состоит** из
таких правил, оказывался бы в `deep` всегда — та же болезнь, от которой лечилась
ступень `wide`.
**Ступень определяет класс правила, а не вид работы.** Миграция схемы сама по
себе `standard` — но миграция, которая **переносит данные** по правилу («сложить
дубли», «привести к одному виду перед сравнением»), несёт правило идентичности и
потому `deep`. Одно и то же слово в описании задачи попадает в разные ступени, и
это не противоречие: смотрят не на слово, а на то, есть ли выбор, которого спека
не сделала.
**Проект, у которого таких правил нет вовсе, `deep` не запускает никогда.** Это
законное состояние, а не признак недонастройки: бывают проекты, где данные
приходят уже нормализованными, ничего ни с чем не сливается, а внешних форматов
нет. Верхняя ступень там просто не срабатывает, и придумывать ей повод не надо.
Свои места проект перечисляет в `docs/review.md`, подраздел «Триггеры профиля» —
поимённо, узлами или capability. Перечень **производен от теста**: он не расширяет
класс, а называет, где этот класс живёт именно здесь.
### Профиль — максимум по поверхности, и отсюда размер задачи
Условия читаются сверху вниз, и **первое подошедшее отвечает за весь дифф**.
Профиль изменения это максимум по его поверхности, а не средневзвешенное: одна
строка в перечне границ задачи поднимает ступень всему остальному, включая ту
часть, которая сама по себе была бы `quick`.
Отсюда следствие, которое дороже любой настройки триггеров: **цена ревью растёт
быстрее размера задачи.** Крупная задача не просто даёт больше диффа — она с
высокой вероятностью зацепит верхнее условие и оплатит верхний профиль целиком.
Обратное тоже верно и тоже не бесплатно: у каждой задачи есть **несокращаемый
костяк из четырёх проходов** (гейт, спеки, код, триаж). Разрезать задачу, обе
половины которой остаются в одном профиле, — значит заплатить костяк дважды за ту
же проверку. Резать стоит там, где разрез **снимает дорогой проход с большей
части диффа**. Шов и правило нарезки живут у того, кто ведёт задачи, — скилл
`av-dev-pm:tasks`, его `references/split.md`. Пути туда конвейер не выносит: за
пределы своего плагина он ходит вызовом скилла, а не файлом.
Профиль объявляется в отчёте. Понижение профиля — решение оркестратора, и оно
попадает в границы покрытия строкой «профиль понижен до X, потому что …».
## Порядок прогона — граф, а не очередь
Профиль отвечает «какие проходы», порядок — «что кого ждёт». Стадии остаются
единицей **состава** (профиль набирается стадиями, см. таблицу выше), но порядок
задают **не их номера**: между стадиями 1–4 настоящих зависимостей нет — ни один
проход не читает вывод другого, — и очередь между ними была бы платой ни за что.
Рёбер три вида, и они разной природы. Путать их нельзя: первое про
**осмысленность** (на красном гейте опиниативный проход не о чем), второе про
**железо**, третье про **деньги**.
| Ребро | Смысл | Между кем |
|---|---|---|
| **зависимость** | B не стартует, пока A не закончил, потому что без A задание B не определено | гейт → все опиниативные; все проходы → триаж |
| **конфликт за ресурс** | A и B не держат машину одновременно; кто из них первый — неважно, направления у ребра нет | проходы, помеченные «держит машину» |
| **барьер стоимости** | дорогое не запускается, пока дешёвое не сказало, что форма изменения выживет | только `deep` |
```mermaid
flowchart TD
gate["gate<br/>(стадия 0, держит машину)"]
specs["specs"]
code["code"]
adversary["adversary<br/>(держит машину)"]
ops["ops<br/>(держит машину)"]
architecture["architecture<br/>(wide, deep)"]
barrier{{"форма изменения выживает?"}}
reimpl["reimpl"]
triage["triage — единственный сток"]
gate -->|зелёный| specs
gate -->|зелёный| code
gate -->|зелёный| adversary
gate -->|зелёный| ops
gate -->|"зелёный, wide и deep"| architecture
adversary -. один ресурс — машина .- ops
specs --> barrier
code --> barrier
adversary --> barrier
ops --> barrier
barrier -->|"deep"| reimpl
barrier -->|"quick, standard, wide: барьера нет"| triage
reimpl --> triage
architecture --> triage
```
Читается граф так: **всё, у чего входящие рёбра закрыты, уходит одним
сообщением**. В `standard` после зелёного гейта это три узла разом — `specs`,
`code` и первый из меряющей пары, — а второй меряющий идёт следом за первым. В
`wide` к этой тройке добавляется четвёртым `architecture`. В `quick``specs` и
`code` разом, и сразу триаж.
**Схема здесь старше прозы.** Она не иллюстрация к тексту, а сам алгоритм
планировщика; проза ниже объясняет рёбра и называет их цену. Разошлись — прав
граф, а расхождение чинится правкой текста.
**Ребро значит «A закончил раньше, чем B стартовал», и ничего больше.** В обычном
графе задач ребро тянет за собой данные — здесь нет, и это не деталь реализации.
Проход **не видит** находок других проходов, в каком бы порядке их ни запустили.
Вся ценность конвейера держится на декорреляции: под всеми ролями одна модель с
одними априорными, и стоит показать ей чужой вывод — она согласится. Согласие
нескольких проходов и так не повышает `confidence` (см. «Честный предел»);
согласие **наведённое** ещё и маскируется под независимое подтверждение.
Единственный, кто получает чужие выводы, — триаж, и это его работа.
### Кто держит машину
Ресурс один и неделимый: **машина** — тесты, поднятый сервис, СУБД, порты, диск.
Проходы, заявившие его, сериализуются между собой в любом профиле и на любой
стадии; порядок внутри цепочки произволен.
| Проход | Держит машину | Почему |
|---|---|---|
| `gate` | да | запускает инструменты проекта — но он источник графа и один по построению |
| `adversary` | да | находка есть **построенный путь**: он пишет падающий тест и гоняет его |
| `ops` | да | доказывает числами: время удержания блокировки, пик кучи, темп роста журнала |
| `triage` | да | проверяет оракул `critical`/`major` запуском — но он сток и тоже один |
| `specs`, `code`, `reimpl`, `architecture`, `rubric` | нет | читают и рассуждают; `reimpl` пишет свою реализацию в черновик, но не исполняет её |
**Правило про ресурс, а не про имена.** Раньше здесь стояло именованное
исключение «`adversary` и `ops`»; оно рассыпается, как только проход начнёт
мерить или в проекте появится свой. Два прохода на одной машине соревнуются за
диск, CPU и за саму СУБД и выдают числа, которые не воспроизведутся, — а число,
снятое под конкурентную нагрузку, это находка с испорченным оракулом. Её
опровержение стоит дороже всего выигрыша от параллельности, и она хуже
отсутствующей: выглядит доказанной. Правило выведено из находок, целиком
державшихся на таких замерах; у каждого проекта они свои и лежат в журнале
`docs/review.md`.
Проект вправе пометить «держит машину» и другой проход — в `docs/review.md`,
разделе настройки конвейера. Снимать пометку с перечисленных нельзя.
### Барьер стоимости — вместо раннего выхода
Барьер существует ровно там, где ранний выход зарабатывал: `reimpl` пишет
реализацию целиком и потому самый дорогой проход конвейера. Если дешёвая часть
нашла, что **форму изменения** надо переделывать, он будет писать её против кода,
которого через час не станет.
- **прошло без находок «переделать форму»** — барьер открыт, `reimpl` уходит;
- **есть такая находка** — прогон останавливается, находка чинится, конвейер
запускается **заново с нулевой стадии**, а не «доезжает» остатком по старому
коду. Незапущенные проходы идут в границы покрытия строкой «не запускался:
прогон остановлен на <проход> из-за <находка>», поимённо. Триаж на половине
прогона не запускается: его отчёт выглядит полным, потому что агрегирует всё,
что ему подали, — это тот же молчащий пропуск, что и в разделе «Профили»;
- **находка чинится в пределах существующей формы** (`Действие: инлайн`) —
барьер не срабатывает: дешевле дособрать все находки и починить пачкой, чем
гонять конвейер дважды.
**`architecture` стоит за барьером только там, где барьер и так есть.** В `deep`
он уходит вместе с `reimpl` — ждать ему всё равно нечего. В `wide` он стартует
сразу после зелёного гейта, в одном ряду со стадиями 1 и 2: своего барьера он не
заслуживает. Потолок в 3 находки делает его дешёвым, а барьер не бесплатен — он
сериализует то, что могло идти разом, и платить сериализацией за один дешёвый
проход не за что. Есть и вторая причина, помельче: барьер спрашивает «выживает ли
форма изменения», а `architecture` — как раз тот, кто на этот вопрос отвечает.
В `quick`, `standard` и `wide` барьера нет — за ним нечего защищать: стадии 3 в
этих профилях не бывает, и граф там плоский от гейта до триажа. Находка «переделать
форму» ловится в них триажем, а прогон после починки повторяется целиком: платить
за это нечем, дорогих проходов в этих профилях нет. В `design` его тоже
нет, и по другой причине: там предметом и является форма, а все три прохода
читают одно предложение — защищать нечего, у графа этого профиля своя форма (см.
его раздел).
### Линеаризация — когда графа мало
Граф можно вытянуть в одну цепочку. Это отступление, и оно называется в отчёте:
1. **сказал оператор** — «гони линейно». Набора называть не надо: линейный прогон
ничего не портит, он только дольше, и домысливать тут нечего;
2. **машина занята, и знает об этом вызывающий.** Рядом идёт другая задача,
поднят сервис, гоняется дорогая проверка проекта. Сам конвейер занятости
машины не видит — её обязан назвать тот, кто запускает; так и делает
`av-dev-pipeline:task-batch`, когда ведёт задачи параллельно;
3. **разбор самого конвейера** — когда выясняется, почему проход чего-то не
нашёл, порядок и изоляция важнее скорости.
Обратное отступление — **слить цепочку ресурса** (пустить меряющие проходы
разом) — бывает только по прямому слову оператора, и тогда в границы покрытия
идёт строка: какие проходы шли одновременно и что замеры этого прогона как
оракул слабее.
Режим объявляется в отчёте наравне с профилем: **`по графу`** — одним словом,
**`линейно`** — с причиной (какой именно из трёх).
## Стадия 0 — Gate (обязательна во всех профилях)
Агент `review-gate`. Запускает команду гейта из семантики гейта в `CLAUDE.md` и
интерпретирует вывод.
**Пока гейт красный — опиниативные проходы не запускаются.** Оркестратор чинит и
перезапускает гейт. Исключение одно: отказ, унаследованный от базовой ветки
(гейт проверяет это прогоном на базе) — тогда он фиксируется находкой и не
блокирует.
Гейт возвращает не только «зелено/красно», но и находки класса **отсутствующая
верификация**: изменённые строки без покрытия, конкурентность без теста с
параллельным доступом, флаки-тест (не ниже `major`), недоступный инструмент.
Шаги выбираются по изменённым файлам: правка документации не гоняет тесты,
линтеры и детектор гонок. Пропуск при этом не молчит — он виден в сводке с
причиной и уезжает в границы покрытия, как и любой другой `SKIP`.
Шаги, которые красят гейт безусловно, перечислены в `CLAUDE.md` с причиной. Проходу
запрещено списывать такой отказ в мелочь.
## Стадия 1 — Conformance (обязательна во всех профилях)
Два applicative-прохода: оба применяют **записанный** критерий, оба дешёвые.
Машину не держат ни один, ребра между ними нет — уходят одним сообщением сразу
после зелёного гейта, вместе со стадией 2, если она в профиле.
- `review-specs` — критерий взят из **дельта-спек предлагаемого изменения**, а не
из proposal, сообщения коммита или описания задачи. Сверка двунаправленная;
направление `code → spec` важнее.
- `review-code` — критерий взят из конвенций проекта, каталог
`docs/conventions/`. Берётся только та их часть, которая **не выражается
правилом**: механизируемое уже проверила стадия 0. Что именно механизировано,
перечисляет `conventions/README.md` — повторять это проходом вредно.
Recall обоих равен длине их источника — это и есть предел applicative-проходов,
ради которого существует стадия 2.
## Стадия 2 — Adversarial и operational (`standard`, `wide`, `deep`)
Два прохода:
- `review-adversary` — находка есть **построенный путь**, а не свойство;
- `review-ops` — постмортем от симптома у владельца сервиса к строке кода.
**Оба помечены «держит машину», поэтому между ними ребро конфликта: они идут
цепочкой, а не разом** (правило и его причина — в «Порядок прогона», раздел «Кто
держит машину»). Направления у ребра нет: кто первый — неважно. Со стадией 1 они
конфликта не имеют и стартуют одновременно с ней; ждать её незачем.
Цепочка не отменяется общим «гони по графу» — она и есть часть графа. Отменяет
её только прямое слово оператора про эту пару, и тогда в границы покрытия идёт
строка, что числа прогона сняты под соседней нагрузкой.
**Эта стадия зарабатывает больше всех остальных вместе, и потому стоит уже в
`standard`, а не только в верхних профилях.** Измерено на пяти задачах подряд: враждебный
проход дал пять из семи выживших находок дозапуска (включая обе верхние);
эксплуатационный — единственный, кто нашёл, что откат бинаря поверх новой схемы
стартует молча. Оба несут внешний оракул по построению: один обязан путь
**прогнать**, второй смотрит ось времени и эксплуатации, которую не смотрит
никто другой.
Материал берётся из документов: `docs/security.md` — враждебному,
`docs/architecture.md`, `docs/research/` и `docs/database.md`
эксплуатационному. Что с чем сшивать и почему — [project-facts.md](references/project-facts.md),
раздел «Сшивать обязаны проходы». Без этих документов стадия вырождается в общие
места.
## Стадия 3 — Independent reimplementation (только `deep`)
Единственный проход, ради которого существует **барьер стоимости**, и
единственное, что отличает `deep` от `wide`.
- `review-reimpl` — пишет свою реализацию, не открывая существующую, затем
диффит по решениям. **Профиль и есть его условие:** `deep` выбирается ровно
тогда, когда вводится или меняется по существу правило идентичности, слияния
или разбора — по тесту из раздела «Идентичность, слияние, разбор»; проектный
перечень мест, где такие правила живут, — в `docs/review.md`, если записан. Это
самый дорогой проход конвейера (его счёт определяется объёмом вывода — он пишет
реализацию целиком), а вне этого случая независимый взгляд в значительной мере
уже дал профиль `design`: код писался под его находки. Условие выбрано по факту:
единственный раз, когда триаж назвал отсутствие `reimpl` дырой покрытия, — это
была задача с новым правилом слияния сущностей.
Такие правила обычно занимают десятки строк, но определяют смысл **всех** данных
проекта. Отсюда особенность верхней ступени, из-за которой её легко выбрать
неверно: самый дорогой проход тратится на самый **маленький** дифф. `deep` не про
размер изменения и не про его опасность — он про класс правила.
Раньше это условие стояло **внутри** профиля, и `deep` означал то семь проходов,
то восемь. Реестр состава, который «проверяется взглядом», проверять было нечем:
у профиля не было одного правильного ответа. Теперь ступеней две — `wide` и
`deep`, — и у каждой состав ровно один.
## Стадия 4 — Global (`wide`, `deep`, `design`)
Агент `review-architecture`. В `deep` стоит **за барьером стоимости** (ждать ему
там всё равно нечего), в `wide` и `design` — в первой волне, сразу после старта
профиля. Машину не держит, с `reimpl` конфликта не имеет: за барьером они уходят
разом.
**Условие этой стадии и есть условие ступени `wide`:** изменение вводит новое
понятие или структурную единицу. Не «изменение крупное» и не «изменение опасное»:
у прохода появляется работа ровно тогда, когда в проекте становится больше
сущностей, чем было, — и тогда осмысленны оба его вопроса. На изменении, которое
ничего не вводит, вопрос «не появился ли второй способ» отвечается «нет» до
запуска, а вопрос «что опытный человек отсюда удалил бы» вырождается во
вкусовщину, которую потом отсеивает триаж.
Получает **вход шире диффа**: дерево пакетов с
назначением, граф внутренних зависимостей, инвентарь существующих концепций.
Команду, которая это готовит, даёт раздел команд `CLAUDE.md`; нет команды —
проход собирает карту сам и говорит об этом в границах покрытия.
Главный вопрос — концептуальная целостность и **второй способ** делать то, что
уже делается. Он же и оправдывает проход: на задаче про пересборку архитектурный
проход нашёл, что новый код был **вторым проигрывателем журнала** со своим
порядком. Второй обязательный вопрос — **что опытный человек отсюда удалил бы**:
слой с единственной реализацией, интерфейс ради мока, незапрошенная
конфигурируемость, подстраховка поверх подстраховки. Потолок — 3 находки плюс
секция «дешевле переделать до мерджа».
## Стадия 5 — Triage (обязательна)
Агент `review-triage`. **Единственный сток графа и единственный, кто агрегирует.**
Входящие рёбра — все запущенные проходы: пока хоть один не вернул отчёт, триаж не
стартует. Получает сырые выводы всех проходов, `git diff`, профиль, режим и
**список запущенных проходов**; возвращает финальный отчёт.
Отсюда же правило, которое иначе выглядит придиркой: **триаж на неполном графе не
запускается**. Прогон, остановленный барьером или ранним выходом, до стока не
доезжает — его отчёт агрегировал бы половину и выглядел бы полным.
Без триажа проходы дают порядка сорока замечаний при единицах существенных.
Потребитель здесь — оркестратор, который **молча реализует** всё, что прочитал:
цена нетриажированного отчёта — не потерянное время человека, а разросшийся от
вкусовщины код.
Порядок: дедупликация по причине → оракул для всего `critical`/`major`
понижение неподтверждённого до гипотезы → отсев вкусовщины → ранжирование по
ущербу × вероятности → потолок 7 пунктов в основном списке.
## Профиль `design` — до кода
Запускается на шаге ревью спек (шаг 4 скилла `av-dev-pipeline:task-pipeline`),
когда change уже
имеет `proposal.md` и дельта-спеки, но кода ещё нет.
**Состав здесь тоже не постоянный, и условие то же самое, что у `wide`:**
изменение вводит новое понятие или структурную единицу.
- **всегда** — `review-specs` в режиме «дизайн ДО кода». Дельта-спеки сверяются
на каждой задаче: это самый дешёвый чекпоинт конвейера, и он ловит то, что на
готовом коде уже не чинят;
- **при новом понятии** — плюс `review-rubric` (фаза 1 без фазы 2: рубрика на
задуманный узел становится приёмочными критериями и уезжает в `tasks.md`) и
`review-architecture` на предложении: можно ли выразить существующими понятиями
**включая конструкции стандартной библиотеки**, — не появляется ли второй
способ. Вопрос «не изобретаем ли то, что уже есть в библиотеке» живёт здесь;
тогда же задаётся вопрос автору дизайна: **«предложи три формы решения и назови
компромисс каждой»** — если ответ показывает, что рассматривалась одна, это
находка.
Причина условия — арифметика, а не экономия на осторожности. Чекпоинт стоит
**на каждой задаче**, поэтому три прохода здесь умножаются на число задач, и при
мелкой нарезке это самая большая статья конвейера. Рубрика же на узел, который не
вводит нового понятия, порождает свойства уже существующего рода — те, что и так
записаны конвенциями и спеками; а `architecture` без нового понятия отвечает «нет»
на свой главный вопрос ещё до запуска (см. «Стадия 4»).
**Граф этого профиля свой, и он плоский.** Гейта нет — кода ещё нет, запускать
нечего; машину не держит ни один проход; сток — не триаж, а шаг 5 пайплайна
задачи, где замечания отрабатываются правкой спек. Триаж здесь не нужен: находок
единицы, и каждая либо правит спеку, либо становится развилкой.
```mermaid
flowchart TD
proposal["предложение: proposal.md + дельта-спеки"]
specs["specs (режим «дизайн ДО кода») — всегда"]
novelty{{"вводит новое понятие<br/>или структурную единицу?"}}
rubric["rubric, фаза 1 → приёмочные критерии в tasks.md"]
arch["architecture на предложении"]
author["вопрос автору: три формы решения и компромисс каждой"]
fix["шаг 5 пайплайна: правка спек, развилки — вопросом в запись"]
proposal --> specs
proposal --> novelty
novelty -->|да| rubric
novelty -->|да| arch
novelty -->|да| author
novelty -->|нет| fix
specs --> fix
rubric --> fix
arch --> fix
author --> fix
```
Смысл профиля: архитектурная находка на готовом коде стоит переписывания и
поэтому игнорируется; та же находка на предложении стоит абзаца обсуждения.
`rubric` живёт **только** в этом профиле. Судить код по критерию, под который он
писался, — корреляция по построению; те же 8–12 свойств уже лежат приёмочными
критериями в `tasks.md`.
## Контракт находок
Единый для всех проходов — [references/finding-contract.md](references/finding-contract.md).
Коротко: заголовок через **последствие**, обязательные поля `Файл`, `Severity`,
`Confidence`, `Оракул`, `Последствие`, `Предложение`, `Найдено проходом`.
`critical` без оракула или построенного пути не существует. Находка без поля
«Последствие» не выводится вовсе.
Каждый проход завершает вывод блоком `## Coverage of this pass`.
## Что происходит с находками дальше
- Оркестратор чинит помеченное `Действие: инлайн` и **не логирует мелочь**.
- `Действие: развилка` — вопросом с вариантами и ценой каждого туда, где проект
держит вопросы (это знает вызвавший пайплайн, а не конвейер). Оркестратор не
останавливается: он урезает изменение до остатка и доводит его.
- Находка не для этого мерджа, но реальная (отложенный `major`, развилка,
решённая «потом»), — не теряется, но **и не заводится здесь**. Конвейер отдаёт
её **списком урожая** в отчёте: формулировка, оракул, провенанс (какой проход,
какой change). Заведение задач принадлежит тому, кто ведёт задачи проекта, —
у него свой формат, своя нарезка и свои правила дублей. Мелочь класса `nit`
идёт в урожай одной пачкой, а не записью на находку.
- `Promote candidates` — по процедуре [references/promote.md](references/promote.md):
находка → конвенция → правило линтера → **удаление формулировки из конвенций**.
Третий шаг обязателен.
- Дефект, проскочивший ревью и всплывший позже, идёт в журнал проекта
([references/review-journal.md](references/review-journal.md)) — сразу, не
ретроспективно: теряется именно причина непоймания.
- **Отчёт триажа сохраняется вместе с изменением** — `openspec/changes/<id>/review/`.
Он единственное, по чему потом видно, что было найдено и что из этого не
заведено: нулевой урожай при непустом отчёте виден сразу.
**Вместе с изменением он и переезжает:** после `opsx:archive` его адрес —
`openspec/changes/archive/<id>/review/`. Кто ищет отчёт после архивации (батч
на финальной сверке, приёмщик на сессии), смотрит **оба** пути; «отчёта нет»
объявляется, только когда пуст и архивный, иначе самый дорогой сценарий
«состав ревью неизвестен, гоняем заново» срабатывает на каждой доведённой
задаче.
## Честный предел
Модель воспроизводит медиану публичного кода, смещённую к популярному и
туториальному: отсюда тяга к интерфейсам ради интерфейсов, лишним мокам и
конфигурируемости, которую никто не просил. **«Идиоматично» и «распространено» —
разные вещи**; проходы обязаны различать их и опираться на поимённое положение
гайда, а не на ощущение частотности.
Согласие нескольких проходов — **не подтверждение**: это один источник,
высказавшийся несколько раз. Совпадение повышает приоритет, но не `confidence`.
Что недоступно **этому** проекту принципиально — перечисляет «Недоступно
проверке» в `docs/review.md`, и оба его подраздела целиком уезжают в границы
покрытия.
Независимо от проекта недоступно:
- поведение внешних систем в их будущих версиях;
- реальный профиль нагрузки и то, что на самом деле лежит в данных;
- завязка внешних потребителей на текущую форму ответа;
- суждение «этой функциональности не должно существовать».
Отдельно и честно: **поимённая сверка с положениями стайлгайдов языка не
задаётся ни одним проходом.** Проход про идиоматичность упразднён, его способные
части переселены (эксперимент против поведения библиотеки и драйвера — в `ops`,
вопрос 8; «не изобретаем ли то, что уже есть в библиотеке» — в `architecture`,
вопрос 1), но различение «идиоматично против распространено» теперь не спрашивает
никто. Класс обратимый — портит форму кода, не данные, — и его надо признавать в
границах покрытия, а не считать проверенным.
Это и есть причина, по которой конвейер готовит ревью, а не заменяет его.
## Ссылки
- [references/project-facts.md](references/project-facts.md) — что нужно проходу
и где это лежит в документах проекта; таблица поразрядной деградации.
- Skill `av-dev-pm:canon` — приведение проекта к канону документов.
- [references/finding-contract.md](references/finding-contract.md) — контракт находок.
- [references/promote.md](references/promote.md) — промоут находка → конвенция → правило → удаление.
- [references/calibration.md](references/calibration.md) — калибровка инъекцией, вердикты keep/retune/drop.
- [references/review-journal.md](references/review-journal.md) — журнал проскочивших дефектов.
@@ -1,92 +0,0 @@
# Откуда проход берёт проектную конкретику
Конвейер общий, находки — проектные. Проход, не знающий, что в этом проекте
нельзя нарушать, чем краснеет гейт и сколько данных реально идёт через узел,
выдаёт правдоподобные общие места: их дорого опровергать и нечем подтверждать.
Отдельного файла-брифа **нет**. Проектная конкретика живёт в документах канона
`av-dev-pm`, и проход читает их напрямую: пути жёсткие, посредник не нужен, а
второй дом для тех же фактов разошёлся бы и выглядел актуальным.
Определение канона — в плагине `av-dev-pm`,
`skills/canon/references/canon.md`. Здесь только карта «что нужно проходу →
где это лежит».
## Карта
| Что нужно проходу | Где лежит |
| --- | --- |
| что система делает и **чего не делает**, граница домена | `docs/passport.md` |
| инварианты **с severity рядом с формулировкой** | `CLAUDE.md`, раздел инвариантов |
| команда гейта, чем краснеет безусловно, чего в нём нет, кто гоняет дорогое | `CLAUDE.md`, семантика гейта |
| что запускать запрещено, с путями; `testdata`; куда писать временное; имя основной ветки | `CLAUDE.md` |
| компоненты и capability, окружение, внешние зависимости поимённо, наблюдатель, характер потока, единые точки проекта | `docs/architecture.md` |
| чем физически лежит запись, что при чтении и записи, настройки с числовым значением | `docs/database.md` |
| периметр, недоверенный вход, из чего строятся пути и ключи, что вне модели | `docs/security.md` |
| измеренные числа **с провенансом**, поведение внешних систем на самом деле | `docs/research/` |
| конвенции прозой и **что уже механизировано** правилом | `docs/conventions/` |
| почему решено так, отвергнутые варианты | `docs/adr/` |
| типовые узлы, типовые ложноположительные, вопросы к проходам, **триггеры профиля**, недоступно проверке | `docs/review.md`, раздел настройки |
| прецеденты: воспроизведённые дефекты с оракулом | `docs/review.md`, журнал |
| нормативное поведение и дельты изменения | `openspec/specs/`, `openspec/changes/<id>/specs/` |
## Сшивать обязаны проходы
Раньше эти факты лежали рядом в одном файле, и соседство работало само. Теперь
они разложены по домам, и **проход обязан собрать их сам** — иначе снимет верное
число и честно понизит находку до гипотезы, потому что сравнить будет не с чем.
Два обязательных стыка:
- **замер + настройка.** «Пик 768 МиБ» — аномалия только рядом со строкой
«запись лежит сжатой и распаковывается целиком»; «блокировка удерживалась
5.019 с» — гарантированный отказ соседа только рядом с известным таймаутом
занятости. Числа в `docs/research/`, настройки в `docs/database.md`, и оба
читает `ops`, `adversary`, `reimpl`.
- **инвариант + обратимость.** severity берётся из `CLAUDE.md`; если её там
нет — она **выводится по обратимости последствия** и помечается «выведена по
обратимости», а не выдаётся за решение проекта.
## Деградация — поразрядная
Документа нет — деградирует то, что из него читалось, и **только оно**. Каждый
проход пишет **свою** строку в границы покрытия; триаж собирает их в один
список и **не сливает в одну строку**: разные пробелы чинятся разным — периметр
пишется руками за десять минут, а числа требуют замера.
**Кто какой документ читает — не здесь.** Полный список читателей ведёт канон
(`Skill av-dev-pm:canon`, его `references/canon.md`, таблица «Кто читает»); ниже —
только **последствие** отсутствия, и оно называет самое дорогое, а не всех
пострадавших. Два списка читателей уже однажды разошлись; второго раза не надо.
| Нет документа | Что деградирует |
| --- | --- |
| `CLAUDE.md` без инвариантов | `critical` по основанию «нарушен инвариант проекта» не присваивается никем |
| `docs/security.md` | `adversary` не знает периметра — формулирует условиями, `critical` не ставит |
| `docs/research/` | числа неизвестны `specs`, `ops`, `adversary`, `reimpl` — формулируют условиями, а `specs` теряет проверку «требование против наблюдения» |
| `docs/database.md` | замер не с чем сравнить: находка не поднимается выше гипотезы |
| `docs/passport.md` | `architecture` теряет границу домена и вырождается в общее мнение |
| `docs/review.md` | `triage` отсеивает вслепую: типовых ложноположительных нет |
| `docs/architecture.md` | «не появился ли второй способ» не проверяется — единых точек не знает никто |
Строка в границах покрытия обязана называть **причину**: «`docs/security.md` в
проекте нет» читается иначе, чем «есть, но периметр не назван». Без причины
строка неотличима от «мы просто не стали» и перестаёт читаться на третьей задаче.
**Документов канона нет вовсе** — проект не приведён к канону. Это не повод
работать вслепую: скажи об этом строкой и предложи `av-dev-pm:canon`. Одна
операция на проект против деградации на каждой задаче.
## Правило чтения
- **Читай в источнике, не по памяти.** Документы правятся по ходу работы, в том
числе этой же задачей.
- **Число без провенанса — условие, а не утверждение.** Число, чей источник по
ссылке не подтвердился, читается как условие и **называется расходящимся**, а
не подменяется догадкой.
- **Пустое, названное пустым, — это факт.** «Внешних зависимостей нет — смотри
на диск и на СУБД» экономит обязательный вопрос. Отсутствие строки — не факт,
а пробел, и его надо назвать в границах покрытия.
- **Свойство, ставшее правилом линтера, из конвенций удалено** и лежит в
перечне механизированного в `docs/conventions/README.md`. Проверять его
проходом — тратить внимание на уже проверенное.
-454
View File
@@ -1,454 +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`: сделана / не доведена / оказалась крупнее
задачи.
- **Задачи закрывает пайплайн внутри каждого сабагента**, шагом 11 — после
коммита работы и **отдельным коммитом учёта**, вызовом 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. Прочитать набор
Набор задан списком (слаги, файлы, описания) — прочитай файл каждой задачи и
связанные спеки и черновики. Задачи-идеи включаются, но помни: сабагент проведёт
их сперва через `opsx:explore`, это тяжелее и чаще упирается в вопрос.
### 2. Спланировать порядок и пересечения (автономно)
Для каждой задачи определи:
- **затронутые capability** — по её описанию и по каталогу актуальных спек
(`openspec/specs/`);
- **жёсткие зависимости**: задача B строится на результате A → A строго раньше B;
- **замеряющая задача** — та, чьё ревью будет доказывать находки **числами**. В
последовательном прогоне это ничего не меняет: она и так идёт одна. В
параллельном она гонится в волне **одна** (обоснование — ниже, в шаге 4).
Помечается здесь, на планировании, а не во время прогона, и **независимо от
режима**: состав волны определяется сейчас, режим может смениться просьбой уже
после плана, а профиль ревью сабагент выберет только внутри задачи — ключевать
волну на ещё не сделанный выбор нельзя. Триггеры — по фактам о задаче, каждый
сам по себе достаточен:
- трогает схему хранилища, миграцию, формат на диске или объём хранимого;
- трогает конкурентность: транзакции, блокировки, фоновые циклы, общее
состояние;
- трогает размер тела, буфер, память, сжатие, ретеншен, темп потока;
- её тема названа в `docs/research/` или в журнале `docs/review.md` как место,
где уже мерили или уже ломалось.
Ни один триггер не сработал — задача не замеряющая, даже если её ревью
окажется `deep`. Профиль про глубину проверки, замеряющая — про соревнование за
железо; это разные вопросы, и совпадают они не всегда;
- **нумерованные артефакты — номера раздаёт оркестратор заранее.** Если проект
нумерует миграции (путь — `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 с обоими чекпоинтами ревью;
- если задаче назначен **номер артефакта** — используй строго его;
- **профиль ревью выбирается по факту изменения.** Батч не повод понижать
профиль: «нас много и мы спешим» — это ровно тот стимул, из-за которого
проходы пропускают;
- **режим прогона проходов ревью — от режима батча**, и его называет 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`: непусто — значит сабагент не довёл шаг 11 до коммита учёта (или
оставил мусор). Не форсируй и не коммить за него: назови задачу в докладе
недоведённой и оставь ветку с 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` на
интегрированный дифф с вопросом «не появился ли второй способ делать то, что
уже делается» — именно он возникает, когда две задачи независимо решали
похожее;
- **заверши триажем.** Он единственный, кто агрегирует, и без него у находок нет
ни оракула, ни пометки `инлайн`/`развилка` — а следующий абзац на неё
опирается. Прогон из двух проходов без триажа — это сырые находки, выданные за
разобранные.
Граф этой сверки — веер в один сток, и он такой же, как у обычного прогона:
```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 и ветки **провалившихся** не трогай — они нужны
для ручного дожатия.
- **Записей учёта батч не трогает** — их правит пайплайн внутри сабагента на
шаге 11. Батч сообщает исход по каждой задаче; если какой-то сабагент дошёл до
коммита, но закрытия не сделал (плагина нет, вызов не разрешился), скажи это
строкой — иначе задача останется открытой молча.
- Доложи кратко:
- **исход по каждой задаче** одним из трёх слов, с хешем коммита;
- **режим прогона** — по одной или волнами, и если волнами, то по чьей просьбе;
порядок задач или состав волн, порядок интеграции, с пометкой, какие задачи
шли по одной как замеряющие;
- вопросы, записанные сабагентами, пачкой;
- что дозапускалось на шаге 5 и почему; шло ли где-то ревью инлайн;
- итог финальной сверки и ссылки на архивные change;
- **`Урожай`** — отложенные находки всех задач одним списком, с провенансом.
Задачи из него заводит тот, кто ведёт задачи проекта, а не батч;
- **отдельно — провалившиеся** задачи с настоящей причиной и путём к
оставленному worktree;
- **границы покрытия сводной строкой**, включая задачи, чьи замеры снимались в
общей волне, и ветки, где ревью шло инлайн.
## Тонкости
- **Изоляция параллельных тестов — цена параллельного режима, и проверяется она
до первой волны.** Прежде чем гнать несколько прогонов разом, убедись, что
тесты не делят фиксированный порт или файл БД (обычно берут временный каталог и
эфемерный порт — тогда ок). Делят — такие задачи гони по одной, даже если
просили параллельно, и скажи об этом строкой: просьба про параллельность, а не
про сломанные тесты. В умолчательном режиме вопрос не встаёт вовсе — это одна
из причин, по которым умолчание такое.
- Поведенческая верификация внутри сабагента поднимает изменение вживую: в
параллельном режиме следи, чтобы соседние worktree не дрались за порты и
рабочие каталоги. Если проект умеет поднимать только один экземпляр — такие
задачи в одну волну не ставь.
- Ревью выполненного — **до** интеграции; это забота
`av-dev-pipeline:task-pipeline` внутри каждого сабагента, дублировать не надо.
- `openspec validate --strict` тоже внутри пайплайна задачи — не пропускай его
своими правками на интеграции.
- Крупная переработка, предложенная ревью внутри задачи, — развилка: не вливай
молча, вынеси в доклад.
- Держи вызывающего в цикле короткими репликами на переходах фаз (план → прогон
задач → интеграция → финальная сверка), но не проси подтверждать механику.
@@ -1,409 +0,0 @@
---
name: task-pipeline
description: Автономно проводит одну задачу через полный цикл Spec Driven Development — от постановки до коммита (opsx explore→propose→ревью спек профилем design→apply→ревью кода→archive→коммит), с обязательными чекпоинтами ревью и докладом об исходе. Использовать, когда просят взять/сделать задачу или довести идею до реализации.
---
# Пайплайн задачи
Оркестратор **одной** задачи по Spec Driven Development: проводит её от
постановки до коммита максимально автономно. Механику не согласовываем — делаем.
Это тонкая обёртка над каноническими скиллами `opsx:explore` / `opsx:propose` /
`opsx:apply` / `opsx:archive` — вызывай их через Skill, не переизобретай их шаги.
Ревью — скилл `av-dev-pipeline:review-pipeline`, он же держит правило выбора
профиля.
## Предпосылки
- **OpenSpec и скиллы `opsx:*` — жёсткая предпосылка, а не опция.** На них стоят
шаги 2, 3, 6 и 8, проход `review-specs` и профиль `design` (они завязаны на
`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`, который этим владеет
(шаг 11). Закрытие как таковое — его работа, и это осознанное решение с
названной ценой: **приёмщик и исполнитель совпали**. Закрытие поэтому **не
окончательно** — человек на сессии возвращает задачу `reopen` с причиной, а
доклад по критериям приёмки становится единственным, по чему приёмка вообще
возможна. Плагина `av-dev-pm` в проекте нет — вызов не разрешится, и тогда
учёт остаётся владельцу, о чём говорится в докладе.
- **Заведением задач из урожая ревью.** Отложенные находки отдаются **списком**
(см. шаг 7); превращать их в задачи — работа того, кто ведёт задачи проекта.
- **Определением ценности.** «Нужна ли эта функциональность» — не вопрос
пайплайна ни на одном шаге.
Пайплайн владеет **своим** определением готовности (ниже) и **сообщает
наблюдаемый исход**. Что с исходом делать дальше — не его дело.
## Наблюдаемые исходы
Ровно три, и каждый обязан быть назван в докладе прямо:
- **сделана** — определение готовности выполнено целиком;
- **не доведена** — с причиной и с записанным вопросом; что именно сделано и до
какой границы, названо явно;
- **оказалась крупнее задачи** — распознаётся **до заведения 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. ревью предложения, профиль design"]
s5["5. отработать замечания + validate --strict"]
s6["6. opsx:apply — код, гейт, поведенческая верификация"]
s7["7. ревью кода, профиль по факту изменения"]
s8["8. opsx:archive"]
s9["9. синк документации — av-dev-pm:docs"]
s10["10. коммит работы — av-dev-git:commit"]
s11["11. закрыть задачу — av-dev-pm:tasks,<br/>вторым коммитом учёта"]
s1 --> triv
s1 -.-> big
triv -->|"нет: идея или мутная постановка"| s2
s2 --> s3
triv -->|"да: шаги 2 и 4 пропускаются"| s3
s3 --> s4 --> s5 --> s6 --> s7 --> s8 --> s9 --> s10 --> s11
```
Два чекпоинта ревью — шаги 4 и 7 — единственные места, где зовётся конвейер;
порядок «сперва коммит работы, потом коммит учёта» на схеме тоже ребро, и оно
обязательное (шаг 11).
Схема — **сводка**: содержание каждого шага в его разделе ниже, и при
расхождении прав текст.
### 1. Прочитать задачу
Задача задана извне (slug, файл, ссылка, описание) — прочитай её и связанные
спеки и черновики. Не задана — попроси у вызывающего; сам в беклог не лезь и
приоритеты не интерпретируй.
Если проект даёт задаче **критерии приёмки**, выпиши их сразу: на шаге 3 они
уезжают в `tasks.md` change. Файл задачи может быть удалён до коммита, а
критерии обязаны его пережить.
Оцени тривиальность (влияет на шаг 4):
- **тривиальная** — локальная правка без изменения поведения, спек и схемы,
решение очевидно. Explore и ревью спек пропускаются;
- **нетривиальная** — новое или изменённое поведение, дизайн-развилки, задеты
инварианты, схема или несколько capability. Полный цикл.
Здесь же — проверка на «крупнее задачи»: если видно, что одним заходом это не
мерджится, объявляй исход **до** заведения 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. (Нетривиальная) Ревью предложения — профиль `design`, ДО кода
Первый чекпоинт. Вызови Skill **`av-dev-pipeline:review-pipeline`** с профилем
`design` и ссылкой на change `<id>`.
**Состав чекпоинта решает конвейер, а не ты**: `review-specs` в режиме «дизайн ДО
кода» идёт всегда, а `review-rubric` и `review-architecture` — только когда
изменение вводит новое понятие или структурную единицу (то же условие, что у
ступени `wide`). Причина в том, что чекпоинт стоит на **каждой** задаче: при
мелкой нарезке три прохода здесь умножаются на число задач и становятся самой
большой статьёй конвейера.
Смысл профиля: архитектурная находка на готовом коде стоит переписывания и
потому игнорируется — та же находка здесь стоит абзаца обсуждения. Если
`review-rubric` запускался, перенеси его рубрику в `tasks.md` как приёмочные
критерии; там же уже лежат критерии от постановки, если они были.
### 5. Отработать замечания ревью предложения
- Мелочь и явные улучшения — правь сам в спеках и дизайне.
- Развилки (компромисс, scope, инвариант) — вопросом в запись, спеки урезаются на
остаток.
- После правок перепрогони `openspec validate --strict <id>`.
### 6. Написать код — `opsx:apply`
Вызови Skill `opsx:apply` для реализации `tasks.md`. Код — по конвенциям проекта
(каталог `docs/conventions/`). Меняешь схему — обнови её описание в
документации тем же change, если проект этого требует: гейт обычно это проверяет.
Прогони гейт и добейся зелёного — он же гейт следующего шага.
**Поведенческая верификация.** Если задача меняет реальное поведение (новый
эндпоинт, разбор входа, схема, форма ответа) — зелёных юнит-тестов мало. Подними
изменение вживую командой из раздела команд `CLAUDE.md` и прогони сценарий.
Пропусти только для чисто внутренних правок без наблюдаемого рантайма.
**Сервис не оставляем лежать.** Если запуск упал — почини или откати до конца
шага.
### 7. Ревью кода — Skill `av-dev-pipeline:review-pipeline`
Второй чекпоинт. Вызови Skill **`av-dev-pipeline:review-pipeline`**, дав ссылку
на change `<id>`, базу диффа, профиль **и режим запуска**.
**Правило выбора профиля живёт в скилле конвейера** (раздел «Профили»), проектные
триггеры — в `docs/review.md`, если записаны. Здесь оно не пересказывается: три
копии одного правила расходятся, и работать будет та, которую прочитали
последней. Помни ровно одно — **профиль выбирается по факту изменения, а не по
ощущению важности**, и посмотри таблицу перед вызовом.
**Режим по умолчанию — `по графу`, и обосновывать его не надо.** Конвейер сам
знает свои рёбра: гейт открывает опиниативные проходы, проходы с пометкой «держит
машину» идут цепочкой (иначе замеры портят друг друга и находка выглядит
доказанной), дорогие generative-проходы ждут барьера стоимости, триаж — сток.
Твоего участия это не требует.
Просить **`линейно`** нужно только по причине, и она называется строкой: так
сказал оператор; машина занята чем-то ещё (в том числе соседней задачей батча);
идёт разбор самого конвейера.
Скилл сам гоняет гейт, нужные проходы и обязательный триаж. Возвращает отчёт с
потолком 7 пунктов, разметкой `Действие: инлайн | развилка` и секцией границ
покрытия.
**Сверь состав прогона с таблицей профилей в скилле, прежде чем коммитить.**
Отчёт обязан называть запущенные проходы **поимённо и с исходом**; непущенный
идёт строкой «не запускался» в границы покрытия. Реестр короткий (4–8 проходов) —
сверка стоит одного взгляда. Почему это правило существует, объясняет раздел
«Профили» скилла конвейера; здесь — само требование.
Отработай так же, как шаг 5: помеченное `инлайн` чини сам и не логируй,
`развилка` — вопросом в запись (он уже сформулирован триажем, его остаётся
перенести). После правок — снова гейт.
**Урожай — списком, не задачами.** Отложенные находки (реальный `major` не для
этого мерджа, развилка, решённая «потом», пачка `nit`) собери в секцию доклада
`Урожай`: формулировка, оракул, провенанс. Задачи из него **заводит не пайплайн**
— у того, кто ведёт задачи проекта, своя нарезка, свой формат и свои правила
дублей. Твоя обязанность — не потерять и передать.
**Границы покрытия из отчёта не выбрасывай** — они уезжают в финальный доклад
сжатой строкой. Отчёт, из которого исчезло «что проверить было невозможно»,
превращается в ложное ощущение проверенности.
**Отчёт триажа сохрани вместе с change (`openspec/changes/<id>/review/`; шаг 8
унесёт его в `openspec/changes/archive/<id>/review/` вместе с change) — это
обязательно, а не «если удобно».** По нему потом видно, что было найдено и что из
этого осталось в урожае. И это единственный **независимый** артефакт о составе
прогона: под оркестратором `task-batch` именно по нему сверяют полноту ревью
ветки, а не по твоей прозе — она написана тем же, кто мог проход и пропустить.
### 8. Архивировать — `opsx:archive`
Вызови Skill `opsx:archive`: change уезжает в архив, дельты вливаются в
актуальные спеки.
### 9. Синк документации
Ревью выполненного — до этого шага. Затем **вызови 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`.
### 10. Коммит
Коммить **в текущую ветку** (`git rev-parse --abbrev-ref HEAD`), сам ветку не
создавай и не переключай, ничего не пушь. При ручном запуске HEAD обычно на
основной ветке — коммит идёт прямо в неё; под оркестратором `task-batch` HEAD на
ветке задачи в изолированном worktree, и делать дополнительно ничего не нужно.
Сообщение — по-русски, скиллом `av-dev-git:commit`, если он подключён (первая строка «что
сделано», тело списком 1–3 пункта, без трейлеров). Одна задача — один осмысленный
коммит.
### 11. Закрыть задачу — **после коммита, не раньше**
**Вызови Skill `av-dev-pm:tasks`** и попроси закрыть задачу как реализованную —
он владеет форматом и двигает строку из набора спринта сам. Путь к его скрипту не
выясняй и индексы руками не правь: мост между плагинами — вызов скилла, а не
путь.
**Порядок обязателен.** Закрытие удаляет файл задачи; сделанное до коммита оно
оставило бы задачу закрытой без единого следа работы, если шаг 10 упадёт.
**Закрытие тоже коммитится — вторым коммитом, тут же.** Удаление файла задачи и
правка индексов (их имена знает `av-dev-pm`, не ты) — это правки в рабочем
дереве, и оставить их незакоммиченными нельзя по трём причинам: `task-batch` следом делает `rebase`
и `worktree remove`, а те откажут на грязном дереве; закрытие, не доехавшее до
основной ветки, оставит задачу открытой молча; и опора «набор спринта под git
показывает, что и когда закрыто» без коммита — пустые слова. Сообщение короткое,
про учёт, а не про работу: `закрыта задача <slug>`. Это второй коммит осознанно:
правило «одна задача — один осмысленный коммит» про работу, а учёт — не работа.
Плагина в проекте нет — вызов не разрешится. Тогда **ничего не выдумывай**:
скажи в докладе, что учёт задач остаётся за владельцем, и назови исход.
**Приёмщик и исполнитель здесь совпадают**, и закрытие не окончательно: человек
на сессии может вернуть задачу (`reopen` с причиной). Поэтому доклад по критериям
приёмки — не формальность, а единственное, по чему приёмка вообще возможна.
Готово — доложи кратко.
- **исход** задачи одним из трёх слов и, если не «сделана», чем ограничен
результат;
- что сделано, какие вопросы записаны и куда;
- ссылка на архивный change и хеш коммита;
- по каждому критерию приёмки, если они были: **оракул и наблюдаемый исход**
это доклад приёмщику, а не отметка «принято»;
- **`Урожай`** — отложенные находки списком (формулировка, оракул, провенанс).
Задачи из него заводит тот, кто ведёт задачи проекта;
- **одна строка границ покрытия**: какой профиль и режим гонялись, какие проходы
не запускались и что проверить было невозможно. Доклад без неё сообщает
«проверено», не сообщая, что именно.
## Тонкости
- **Не завязывайся на основную ветку и корень репозитория.** Скилл работает в
текущем worktree и на текущей ветке: не делай `git checkout`/`switch`, не
создавай веток, не пушь.
- Не пропускай `openspec validate --strict` перед архивацией.
- Тривиальная задача: шаги 2 и 4 пропускаются; ревью кода (шаг 7) остаётся
всегда, но в профиле `quick`.
- Гейт блокирует: пока он красный, опиниативные проходы не запускаются. Чинить и
перезапускать, а не «посмотреть заодно».
- Если ревью предлагает крупную переработку — это развилка: не правь молча и не
спрашивай, запиши вопросом и доведи остаток.
- Держи вызывающего в цикле короткими репликами на переходах фаз, но не проси
подтверждать механику.
- **Занизить профиль ревью или пропустить проход — самый дешёвый способ
«ускориться», и он же самый дорогой по последствиям.** Защита одна: профиль
выбирается по факту изменения, состав сверяется поимённо, а непущенное
называется в отчёте строкой.
-8
View File
@@ -1,8 +0,0 @@
{
"name": "av-dev-pm",
"description": "Управление продуктом: канон документов проекта (паспорт, архитектура, схема БД, безопасность, конвенции, разведка, ADR, журнал ревью), задачи и цели вместо приоритетов, спринт под одну цель с заморозкой набора, старт нового проекта интервью по брифу и приведение существующего к канону. Не выполняет задачи — этим занимается пайплайн проекта.",
"author": {
"name": "Anton Vakhrushev",
"email": "anwinged@gmail.com"
}
}
-185
View File
@@ -1,185 +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 -->
| Метафора-жаргон | Прямо |
| --- | --- |
| рычаг (кэша, отбора) | условие отбора, параметр |
| навешен не на тот счётчик | завязан не на тот счётчик |
| переширокий матчинг по имени | слишком грубое сопоставление по имени, слишком много слабых совпадений |
| костыль | временное решение, обходной путь — и в чём именно |
| просело, отвалилось | стало медленнее на столько-то, перестало отвечать |
Проверка: **фраза требует, чтобы читатель додумал образ, — заменяется буквальным
описанием того, что происходит.**
<!-- /копия: язык-жаргон -->
7. **Термин, которого нет в документах проекта, вводится одной строкой или не
употребляется.** Заменять его своей догадкой нельзя: ты не знаешь предметную
область. Пиши «термин «X» не встречается ни в документах, ни в других
поданных файлах — введи строкой или назови известным словом».
**Слово, занятое в другом смысле, — та же находка.** Термин, который в одном
документе проекта значит одно, а здесь другое, ломает оба; назови оба места.
## Чего ты не проверяешь
Не своё бывает двух разных родов, и поступают с ними по-разному.
**Чужому подрядчику — строкой в границах покрытия.** Форма записи задачи у
`task-form`; увидел — назови в конце одной строкой, чтобы находка не пропала, но
находкой не оформляй.
**Машинной проверке — вообще ничего.** Всё, что ловят `tasks.py check` и
`docs.py check` (состав и написание секций, наличие разделов, число критериев,
теги, тег `question` при непустом разделе «Вопросы», согласованность индексов,
битые ссылки), **не пиши даже строкой**: это не потерянная находка, а уже
проверенное. Повторять машинную проверку словами — заводить второй дом для
одного правила.
**Содержание**: верно ли решение, нужна ли задача, полна ли архитектура. Это
разбор, а не вычитка, — и о нём тоже молчи.
## Порог вмешательства
<!-- копия: порог-правки из av-dev-pm/skills/canon/references/language.md -->
**Правка без нарушенного правила не делается.** Текст, переписанный «чтобы
звучало лучше», обесценивает список замечаний: когда половина из них вкусовая,
перестают читать весь список, и вместе с ним пропадают настоящие находки.
Сомневаешься — не правь. Формулировка, которая просто **не твоя**, — не находка.
**Систематичность нарушения — не довод в его пользу.** Одна и та же ошибка в
пяти файлах не становится «принятым стилем»: чаще это значит, что правило не
применялось вовсе, — и находка тем важнее. «Так сделано везде» годится как
основание для **одной находки на весь набор** («правило N нарушено в пяти
записях, перечень: …»), но не как основание промолчать. Принятым считается
только то, что назвал зовущий или что записано в конвенциях проекта.
<!-- /копия: порог-правки -->
Одна запись может дать несколько находок, но заголовок правится один раз: не
предлагай два варианта на выбор, предлагай лучший.
## Доклад
Находки по одной, в порядке важности: залог и оценки → жаргон и англицизмы →
стоп-слова. Первые меняют, **что** читатель понимает; последние — только сколько
он на это тратит.
```
<файл>
правило: <номер и короткое имя>
сейчас: <как написано>
предложение: <готовая формулировка, подставляемая как есть>
почему: <одна фраза>
```
В конце — **границы покрытия**: сколько файлов просмотрено из скольких, какие не
смотрел и почему, и по чему проверялись термины (документы проекта названы или
нет). Отчёт без этой строки читается как «всё вычитано», не сообщая, какая часть
осталась нетронутой. Туда же — строка «замечено не по моей части», если бросилась
в глаза форма записи; машинно проверяемое в неё **не идёт**.
Ничего не нашёл — так и скажи одной строкой. Пустой доклад с границами покрытия
полезнее выдуманной находки.
-198
View File
@@ -1,198 +0,0 @@
---
name: canon
description: Привести проект к канону документов av-dev и держать его в соответствии — три операции одной машиной сравнения. check — что разошлось с текущей версией канона; adopt — перевод проекта из любой прежней раскладки (docs/specs, drafts, backlog, BRIEF.md, review-brief) в канон с переносом файлов; upgrade — повышение проекта с версии канона N до текущей по журналу версий. Использовать, когда просят проверить документацию проекта, перевести проект на канон, обновить его под новую версию канона или когда пришли в старый проект и надо понять, что в нём не так. Заведение нового проекта с нуля — скилл init.
---
# Приведение проекта к канону
Три операции, одна машина сравнения с разными исходами:
| Операция | Когда | Исход |
| --- | --- | --- |
| `check` | начало сессии, шаг синка, гейт | что разошлось |
| `adopt` | проект в чужой раскладке | перенос в канон |
| `upgrade` | канон вырос, проект отстал | по журналу версий |
**Определение канона — [references/canon.md](references/canon.md).** Здесь оно не
пересказывается: два описания одной раскладки разъедутся, и работать будет то,
которое прочитали последним. Прочитай его **до** первой правки.
- [references/skeletons.md](references/skeletons.md) — **что именно класть** в
каждый незаполненный слот. Своей формой заглушку не выдумывай: `docs.py`
узнаёт только плейсхолдер `<!-- заполнить: … -->` из шаблонов.
- [references/language.md](references/language.md) — **как это написано словами**:
информационный стиль, применённый к проектным текстам, таблицы англицизмов и
жаргона. Раскладка отвечает, где текст лежит, — этот файл отвечает, каким он
должен быть. Правила общие для документов канона, задач, решений ADR и
записок разведки; вычитывает их отдельным проходом агент `doc-wording`.
- [references/changelog.md](references/changelog.md) — журнал версий канона.
## Три правила, из которых всё следует
1. **Сперва карта, потом файлы.** Человеку показывается, что найдено, как
разложилось и **что не разложилось**, — и только после подтверждения
переносится хоть один файл. Массовый перенос без подтверждения разгребать
дороже, чем согласовать.
2. **Ничего не терять.** Содержимое переезжает целиком; ссылки чинятся тем же
проходом, что и перенос. Старый файл удаляется **только** после того, как
всё его содержимое нашло дом, и это названо поимённо.
3. **Что не классифицировалось — назвать.** Проглоченный абзац выглядит как
«всё перенеслось». Список «не разложилось» идёт в доклад целиком, с причиной
по каждому пункту.
## Инструмент
```
ds="$CLAUDE_PLUGIN_ROOT/skills/canon/scripts/docs.py"
python3 $ds check --dir <корень> [--base <rev>] # раскладка, ссылки, версия, сверки
python3 $ds version --dir <корень> # версия канона скрипта и проекта
```
**Коды выхода — тот же словарь, что у `tasks.py`:** 0 сошлось, 1 дрейф, 2 ошибка
употребления, 3 окружение, 4 внутренний сбой. Ветвись на коде, а не на тексте.
Различать 1 и 3 обязательно: «дрейф раскладки» — рабочая ситуация, «это не
корень проекта» — нерабочая.
### Граница механизируемого — объявляется вслух
Скрипт печатает её сам последним абзацем, и **эту строку из доклада выбрасывать
нельзя**. `check`, отчитавшийся «канон соблюдён» на проекте, где из шести файлов
три лишние, хуже отсутствующего.
Машина **дрейфом** считает: отсутствующий путь канона, файл вне канона, битую
ссылку, отставшую версию, capability без упоминания в обзоре, миграцию без правки
`database.md`. **Замечанием** — незаполненный плейсхолдер и слабое упоминание
capability: незаполненный канон это переходное состояние, а не отказ. Маркеры
долга просто считает числом.
**Ты** судишь о том, чего она не умеет:
- **смысловой дубль** — `docs/specs/recognition.md` описывает то же, что
capability `recognition`. Файлы разные, содержание одно;
- **поведение, оставшееся в `architecture.md`** — раздел на 900 строк с
требованиями вместо обзора;
- **достаточность честной строки** — «внешних зависимостей нет» это факт,
«TBD» — пробел;
- **протухший факт** — документ ссылается на то, чего в коде уже нет.
## `check`
1. `docs.py check`, при наличии базы диффа — с `--base`.
2. Прочитай то, что скрипт проверить не может (список выше), по документам,
которых касалась работа. Не «заодно по всему `docs/`».
3. Доклад: вывод скрипта строкой исхода, твои находки поимённо, **граница
покрытия** — что смотрел и чего не смотрел.
Дрейф раскладки чинится переносом; смысловые находки — это либо правка
документа, либо задача, если работы больше чем на абзац.
## `adopt` — проект в чужой раскладке
### 1. Осмотрись
`docs.py check` — он уже назовёт упразднённые слоты с адресом, куда каждый
уезжает. **Но смотрит он только верхний уровень `docs/`:** упразднённое в корне
репозитория (`BRIEF.md`) и во вложенных каталогах он не назовёт никогда, поэтому
корневые `*.md` читаются глазами. Плюс: `CLAUDE.md`, `openspec/specs/` (список
capability), `openspec/config.yaml`.
### 2. Составь карту
Каждый найденный файл получает строку: **куда едет, целиком или разбирается, что
делать с оригиналом**. Разбор `docs/specs/` — самое дорогое место, и он делается
поимённо по capability:
| Что в файле | Куда |
| --- | --- |
| требования, сценарии, поведение | `openspec/specs/<capability>/spec.md`**или уже там**, тогда файл дубль |
| компоненты, транспорты, раскладка, деплой | `docs/architecture.md` |
| конвенции чужой системы, формат чужих данных | `docs/research/` |
| обоснование принятого решения | `docs/adr/` |
**Дубль удаляется только после поимённой сверки**: открыть спеку capability,
открыть файл, убедиться, что в файле нет ничего сверх спеки. Нашлось сверх —
сперва переезжает в спеку дельтой, потом файл удаляется.
### 3. Покажи карту человеку
`AskUserQuestion`, **не больше трёх вопросов за итерацию**, рекомендация первым
вариантом. Показывается: сколько файлов, куда каждый, спорные отнесения, список
«не разложилось». Механику (порядок строк, имена файлов внутри `research/`) не
выноси — это не развилка.
### 4. Перенеси
Порядок важен — он минимизирует окно, в котором ссылки битые:
1. `docs/.pm.json` с `{"canon": <текущая версия>}` и путём миграций, если БД есть;
2. каталоги канона и скелет **по [references/skeletons.md](references/skeletons.md)**:
незаполненное — одной честной информативной строкой, а не «TBD»;
3. переносы содержимого;
4. каталог задач — **вызови скилл `av-dev-pm:tasks`**, сценарий адаптации: он
владеет форматом задач, включая переименование транслитных слагов в
английские вместе с починкой перекрёстных ссылок;
5. починка ссылок на перенесённое во всём репозитории — `docs/`, `openspec/`,
`CLAUDE.md`, `README.md`;
6. удаление оригиналов — **только тех, чьё содержимое найдено в новом доме**;
7. **шаг `docs.py check` в гейт проекта.** Путь к скрипту — переменной с
умолчанием на канонический путь маркетплейса, чтобы переустановка плагина не
меняла `Taskfile`; шаг обязан **краснеть внятно**, если скрипт не найден, а не
пропускаться. Передай ему базу диффа (`--base`) той же переменной, что и
остальным шагам гейта: без неё сверка миграций со схемой не гоняется вовсе.
Пример строки покажи человеку — гейт принадлежит проекту, и правит его он;
8. `docs.py check` — до **отсутствия дрейфа раскладки**. Замечания
(незаполненные плейсхолдеры, слабое упоминание capability) остаются:
незаполненный канон это объявленное переходное состояние из шага 5, а не
отказ. **Пункт «задачи без цели» из вложенной проверки `tasks.py` тоже
остаётся** и зелёным на этом шаге не станет: цели не сочиняются адаптацией
(запрет в [tasks/references/adopt.md](../tasks/references/adopt.md)), их
проставляет человек порциями переоценки на первой сессии. Пересчитай эти
пункты в докладе переходного состояния — не выдавай их за поломку и не
молчи о них.
### 5. Объяви переходное состояние
Сразу после переноса канон **заполнен не весь**, и это нормально, но обязано
быть названо, иначе следующий агент примет скелет за поломку.
Печатается по факту: сколько документов стоят честной строкой вместо
содержания, сколько маркеров долга в `architecture.md`, сколько задач без
критериев приёмки. Закрывается порциями по ходу работы, а не одним заходом.
## `upgrade` — канон вырос
1. `docs.py version` — версия проекта и версия скрипта.
2. Проект новее скрипта — **обнови маркетплейс**, а не проект: это отстал
плагин.
3. Иначе иди по [changelog.md](references/changelog.md) снизу вверх от версии
проекта до текущей и делай названное в каждой записи. Записи независимы и
применяются по порядку.
4. Подними `canon` в `docs/.pm.json` до текущей.
5. `docs.py check`.
Записи журнала описывают **что сделать проекту**. Если запись этого не говорит —
это дефект журнала, и о нём надо сказать, а не догадываться.
## Чего этот скилл не делает
- **Не сочиняет содержание.** Пустой слот получает честную строку о том, что его
наполнить пока нечем, а не выдуманный абзац. Придуманный периметр модели угроз
хуже отсутствующего: по нему будут строиться находки.
- **Не удаляет то, чьё содержимое не нашло дом.** Оригинал живёт, пока не
названо поимённо, куда переехал каждый его кусок.
- **Не ведёт содержимое канона** — это скилл `docs`. Здесь только раскладка.
- **Не заводит проект с нуля** — это скилл `init`.
- **Не правит историю.** В старых коммитах старые пути остаются, и это нормально.
## Доклад
- Что нашёл `docs.py`: код выхода и число пунктов дрейфа.
- Что перенесено: файл → дом, числом и поимённо для спорного.
- **Удалённые дубли** — с указанием, против какой спеки сверялся каждый.
- **Не разложилось** — поимённо, с причиной.
- Переходное состояние числами: честных строк, маркеров долга, задач без
критериев.
- **Граница покрытия**: что проверила машина, что судил ты, чего не смотрел
никто.
-353
View File
@@ -1,353 +0,0 @@
# Канон документов проекта
**Версия 3.**
Это **единственный дом определения канона**. Скиллы `init`, `canon` и `docs`
читают его, а не пересказывают: три описания одной раскладки разъедутся, и
работать будет то, которое прочитали последним. Меняется канон — меняется этот
файл и появляется запись в [changelog.md](changelog.md).
## Зачем канон жёсткий
Пути фиксированы, и проект под них подгоняется, а не наоборот. Причина не
техническая: проектов много, все малого и среднего размера, и ориентироваться в
слегка похожих, но разных раскладках дороже, чем один раз привести их к общей.
Рядом лежит OpenSpec, у которого структура тоже строгая.
Цена принята сознательно: плагин не переносится на чужой репозиторий как есть —
чужой репозиторий **приводится** к канону скиллом `canon`.
Раскладка отвечает, **где** текст лежит и на какой вопрос отвечает. Каким он
должен быть **словами** — общий для всех документов канона файл
[language.md](language.md): информационный стиль, англицизмы, жаргон. Он
относится и к задачам, и к решениям ADR, и к запискам разведки.
## Раскладка
```
CLAUDE.md памятка агенту: что это, стек, инварианты с
severity, команды, семантика гейта, запреты
docs/
.pm.json версия канона и пути, нужные проверкам
passport.md зачем и для кого; чем НЕ является; сценарии
architecture.md как сложено — обзор; окружение и эксплуатация
database.md схема хранилища; представление данных и настройки
security.md периметр; недоверенный вход; что вне модели
conventions/
README.md индекс, правило промоута, что механизировано
<тема>.md
research/
README.md как снималось, индекс
<тема>.md наблюдения и числа с провенансом
adr/
README.md индекс записей, статусы, правило замены
template.md
ADR-ГГГГ-ММ-ДД-slug.md
review.md настройка конвейера под проект + журнал дефектов
tasks/ скилл tasks: items/, ROADMAP.md, BACKLOG.md,
SPRINT.md, REJECTED.md
openspec/
config.yaml только нужды генерации артефактов + ссылки
specs/<capability>/spec.md что система делает — нормативно
changes/archive/ архив изменений с design.md — сырьё для ADR
```
Текст документов — русский; слаги файлов, capability и задач — английские,
kebab-case.
## Роли документов
Одна строка на каждый — на какой вопрос он отвечает и кто его читает.
| Документ | Вопрос | Кто читает, кроме человека |
| --- | --- | --- |
| `CLAUDE.md` | что нельзя нарушать, чем краснеет гейт | все агенты, всегда |
| `passport.md` | зачем и для кого, чем это **не** является | `architecture`, `rubric`, `reimpl`, `specs` |
| `architecture.md` | как сложено и где что работает | все проходы ревью |
| `database.md` | что лежит в хранилище и какими настройками | `ops`, `adversary`, `reimpl` |
| `security.md` | против кого защищаемся и что вне модели | `adversary` |
| `conventions/` | как мы пишем код | `code` |
| `research/` | что показала реальность, а не документация | `specs`, `reimpl`, `ops`, `adversary` |
| `adr/` | почему решено именно так | `architecture` |
| `review.md` | как настроен конвейер и что уже проскакивало | `triage`, каждый проход — свою часть |
| `openspec/specs/` | что система делает — нормативно | `specs` |
### `passport.md`
Цель; закрытый список потребителей и что каждому нужно; **чем целью не
является** — это граница домена, по которой архитектурный проход судит о
переносе понятия; типовые сценарии; мера, по которой проект считается удавшимся;
референсы, у кого подсматривать.
### `architecture.md` — **обзор, не поведение**
Принципы; компоненты **со ссылками на capability**, а не с пересказом их
требований; **единые точки проекта** — где генерируются идентификаторы и время,
где единственный парсер входного формата, где маппинг доменной ошибки в код
ответа, где общий путь приёма (это материал для вопроса «не появился ли второй
способ»); внешние границы и форматы чужих систем; окружение — где работает, что
рядом, кто перезапускает; **внешние зависимости поимённо** и чем каждая
отказывает (не только «падает», но и «отвечает медленно», «молчит», «отдаёт
мусор»); кто заметит отказ и когда; характер потока — непрерывный, по запросу,
по расписанию; деплой; открытые вопросы.
**Обратимости здесь нет** — её единственный дом `CLAUDE.md`: туда ходят пять
проходов, и раздвоение адреса означало бы, что проект написал ответ, а ревью его
не прочитало.
**Поведение системы сюда не пишется.** Его нормативный дом — `openspec/specs/`,
куда `opsx:archive` вливает дельты; второй дом синхронизировать руками
невозможно, и он разойдётся.
Раздел, ещё не разнесённый при переезде, помечается маркером долга:
```
<!-- канон: поведение → openspec/specs/<capability> -->
```
`docs.py` считает маркеры и печатает остаток числом. Гейт от них **не краснеет**:
это долг, а не отказ, иначе постепенный переезд стал бы невозможен.
### `database.md`
Схема: таблицы, ключи, связи, правило времени и идентификаторов. Плюс то, чего
нет в схеме, но без чего замер не превращается в находку: **чем физически лежит
запись** (сжатый BLOB, JSON-строка, колонки), что происходит при чтении и записи
(распаковка целиком, read-modify-write), и **настройки с числовым значением**
таймаут занятости, режим журналирования, лимит тела, размер пула, ретеншен.
Конвенции идентификаторов и именования — не схема, они в `conventions/`.
### `security.md`
**Периметр первой строкой.** «Сервис открыт наружу» и «контур доверенный,
публичного интернета здесь нет, не выдумывай его» — противоположные постановки
под одним заголовком, и враждебный проход между ними сам не выберет. Контур ещё
не развёрнут — назови **оба** периметра, целевой и сегодняшний, и скажи прямо,
против какого строятся находки.
Дальше: что недоверенное и каким каналом приходит; **из чего строятся пути и
ключи** (раскладка файлов, состав координатного ключа, имя каталога) — отсюда
строится выход за пределы песочницы; что разграничивает доступ; что
чувствительнее чего; **что вне модели** — перечислить явно.
### `conventions/`
Прозой остаётся **только то, что не выражается правилом**. `README.md` держит
индекс, правило промоута и **перечень уже механизированного** со ссылкой на
место механизации — конфиг линтера, собственный анализатор, тест-сканер
исходников. Непойманное место механизации означает, что проход добросовестно
проверит уже проверенное.
### `research/`
Наблюдения за внешним миром: что реально шлёт источник, чем документация формата
расходится с практикой, какие числа сняты с живого потока. **Числа — с
провенансом**, то есть с командой или условиями, которыми получены.
`README.md` — как снималось и индекс тем.
Число без источника проход обязан читать как условие, а не как замер. Число, чей
источник по ссылке не подтвердился, не выбрасывается и не переписывается по
догадке — остаётся с пометкой «расходится с источником: там <что нашли>».
### `adr/`
**ADR — промоут поверх архивных `design.md`, а не второе сочинение.** Запись
цитирует решение и ссылается на `openspec/changes/archive/<id>/design.md`.
Заводится, когда верно одно из трёх:
<!-- дом: adr-когда-заводить -->
- **дорогой откат** — переделка стоит дороже переписывания одного файла;
- **намеренный отказ** от очевидного подхода;
- **пересмотр прежнего решения** — тогда у старой записи обязателен статус
«заменено на».
<!-- /дом: adr-когда-заводить -->
Не заводится для рутины и для того, что видно из кода и `git log`.
Записи неизменяемы: передумали — заводится новая, старая получает статус.
Активная запись статуса не имеет.
**Статус живёт полем меты записи**, там же, где дата и источник:
`- **Статус:** заменено на ADR-…` либо `- **Статус:** устарело`. Места ему в
шаблоне не отводилось, и каждая запись изобретала своё — то абзацем, то
заголовком; в таблице `adr/README.md` статус при этом обязан быть, а брать его
оттуда, где он у каждого свой, нельзя.
### `review.md`
Два раздела с разными сроками жизни.
**Настройка конвейера под проект**, пять подразделов с точными именами — по ним
проходы находят свой кусок:
- **Типовые узлы** — рода узлов проекта и 3–5 проверяемых свойств к каждому;
- **Типовые ложноположительные** — находки, которые здесь выглядят убедительно и
всегда неверны, каждая со строкой «почему здесь это не дефект»;
- **Вопросы к проходам** — поимённо, в форме `<имя прохода>: <вопрос>
(<провенанс>)`;
- **Триггеры профиля** — проектная конкретизация правила выбора профиля ревью:
что в этом проекте считается **новым понятием или структурной единицей** (это
поднимает прогон до `wide`) и **где живут правила идентичности, слияния и
разбора** (до `deep`) — перечнем мест, производным от теста конвейера, а не
вторым определением класса. Уточняет умолчания, а не отменяет их. Рабочее
умолчание — `standard`: миграция схемы и публичный контракт ступень **не**
поднимают, их проверяют проходы, которые в `standard` и так есть;
- **Недоступно проверке** — два подраздела: «не проверит ни один проход»
(принципиальная граница, по факту промаха не пересматривается) и «перестали
проверять сознательно» (пересматривается первым).
**Журнал дефектов:** запись на каждый воспроизведённый дефект с пометкой
**проскочил / пойман ревью**. Проскочившие — эвал-сет для калибровки конвейера,
выборка по пометке. Пойманные с оракулом — лучшая опора для прохода: проектные,
воспроизводимые, однажды оказавшиеся правдой.
### `tasks/`
Раскладку, форму записи и команды держит скилл `tasks` — канон фиксирует имена
файлов (`items/`, `ROADMAP.md`, `BACKLOG.md`, `SPRINT.md`, `REJECTED.md`) и то,
от чего зависит, читается ли проект как продукт.
**`ROADMAP.md` отвечает на «что приложение уже умеет и чего ещё не умеет».** Это
не очередь работ: цель — **возможность приложения**, задача — шаг к ней.
Достигнутая цель из роадмапа **не исчезает** — строка с датой переезжает в
секцию достигнутого, потому что «что умеет» и есть половина вопроса, ради
которого документ открывают. Вторым домом поведения роадмап при этом не
становится: нормативное поведение живёт в `openspec/specs/`, роадмап отвечает,
**когда и в каком порядке** оно появилось.
Плюс два требования к записи задачи, потому что от них зависит, можно ли её
оценить:
- **род работы** тегом `kind:<род>` из закрытого словаря `feature` | `fix` |
`chore` | `research` — у задачи обязателен, у цели запрещён. Он же решает,
нужна ли цель: у `feature` обязательна, у остальных нет;
- **раздел «Затрагивает»** в теле задачи — границы, которых изменение касается
(эндпоинт, таблица и миграция, формат на диске, публичный тип пакета).
Оба требуются **к взятию в спринт**, а не к заведению: беклог пополняется чаще,
чем разбирается, и требование на входе выгоняло бы в заметки то, что должно
лежать задачей.
### `CLAUDE.md`
Что это и стек; **инварианты с severity рядом с формулировкой** — по ним проходы
присваивают `critical`, поэтому severity стоит здесь, а не выводится каждым
проходом заново; команды; **семантика гейта** — чем краснеет безусловно и почему,
где логи, что означает исход, чего в гейте намеренно нет, **кто и когда обязан
гонять дорогое вне гейта**.
Плюс то, что нужно git-операциям и проходам и не выводится ниоткуда:
- **имя основной ветки** — от неё считается база диффа
(`git merge-base HEAD <ветка>`), в неё вливает батч, от неё ветвятся задачи.
Угадывание между `master` и `main` ломает интеграцию целиком;
- **что запускать запрещено, с путями** — рабочая БД, боевой каталог данных,
внешние сервисы. Запретом с путями, а не «будь осторожен»;
- **где `testdata`** и что в них лежит; **куда писать временное**;
- **что считается необратимым** — единственный дом: от обратимости зависит вся
шкала ранжирования триажа и право проходов на `critical`;
- **общий станок**, врывающийся в замороженный спринт; **ориентир по размеру
спринта**.
### `openspec/config.yaml`
**Только нужды генерации артефактов** — язык, правила именования capability,
придирки валидатора RFC 2119 — плюс ссылки на документы канона. Правило ревью,
пересказ конвенций и инварианты сюда не пишутся: у них есть свои дома, и второй
дом разойдётся на первой же правке.
## Правило единственного дома
Факт живёт ровно в одном файле; остальные ссылаются. Карта на случай спора:
| Факт | Дом |
| --- | --- |
| поведение системы | `openspec/specs/<capability>/spec.md` |
| почему решено так | `adr/`, источник — архивный `design.md` |
| граница домена, «чем не является» | `passport.md` |
| инвариант и его severity | `CLAUDE.md` |
| что приложение умеет и чего не умеет; порядок работ | `docs/tasks/ROADMAP.md` |
| измеренное число | `research/` |
| настройка с числовым значением | `database.md` |
| периметр и модель угроз | `security.md` |
| что необратимо | `CLAUDE.md` — **не** `architecture.md` |
| единые точки проекта | `architecture.md` |
| имя основной ветки, `testdata`, временный каталог | `CLAUDE.md` |
| что уже механизировано правилом | `conventions/README.md` |
## Пустое называется пустым
Скелет канона заводится **целиком** с первого дня. Незаполненный документ держит
**одну честную информативную строку**, а не заглушку:
- «внешних зависимостей нет — смотри на диск и на СУБД»;
- «наблюдений на живых данных нет: внешний источник один, формат документирован»;
- «прецедентов не накоплено»;
- «сознательно ничего не отключали»;
- «архитектуры пока нет: кода нет, заводится первой задачей».
Проход читает такую строку **как факт** и не тратит на неё обязательный вопрос.
Отсутствие файла он не может прочитать никак, а «TBD» читает как пробел —
поэтому `docs.py check` отличает честную строку от нетронутого плейсхолдера
шаблона и напоминает о втором.
## Слотов нет
Файлы и каталоги, которых в каноне **нет**, и куда уезжает их содержимое:
| Было | Куда |
| --- | --- |
| `docs/review-brief.md` | документы канона и есть бриф; остаток — в `review.md` |
| `docs/specs/` | `openspec/specs/` (поведение) и `architecture.md` (обзор) |
| `docs/drafts/` | идея → задача `[idea]`; отказ → ADR; порядок → `ROADMAP.md`; размышление → `opsx:explore` |
| `docs/plan.md` | `docs/tasks/ROADMAP.md` |
| `BRIEF.md` | `passport.md` |
| `docs/backlog/` | `docs/tasks/` |
| `docs/review-journal.md`, `docs/review/journal.md` | `docs/review.md` |
## Что проверяет машина, а что человек
Граница объявляется вслух в каждом отчёте: `check`, отчитавшийся «канон
соблюдён» на проекте, где из шести файлов три лишние, хуже отсутствующего.
| Проверяет `docs.py` | Судит агент |
| --- | --- |
| отсутствующие пути канона | смысловой дубль документа и capability |
| файлы в `docs/` вне канона | поведение, оставшееся в `architecture.md` |
| битые относительные ссылки | протухший факт, разошедшийся с кодом |
| версия канона и её отставание | достаточность честной строки в пустом слоте |
| нетронутый плейсхолдер шаблона | связность и читаемость |
| маркеры долга — числом | |
| миграция изменена, а `database.md` нет | |
| capability без упоминания в `architecture.md` | |
## `docs/.pm.json`
```json
{
"canon": 2,
"migrations": "internal/store/migrations",
"tasks": {
"backlog": "INDEX.md"
}
}
```
`canon` — версия канона, под которую проект приведён, целым числом: обратной
совместимости у канона нет, есть «приведён» и «не приведён». `migrations` — путь
каталога миграций, если БД есть; по нему `docs.py` делает сверку с
`database.md`. `tasks` — настройки каталога задач, переехавшие сюда из прежнего
`<tasks>/.tasks.json`: **один конфиг на весь канон, а не по одному на каталог**.
Внутри `tasks` — **только имена файлов и заголовков** (`items`, `backlog`,
`plan`, `sprint`, `rejected`, `sprint_section`, `questions_heading`,
`criteria_heading`, `oracle_word`), и ключ пишется, лишь когда имя отличается от
умолчания. **Секций беклога здесь нет:** их дом — заголовки `##` самого индекса,
и второй список сразу разошёлся бы с первым. Неизвестный ключ `tasks.py`
отвергает кодом 3, поэтому лишнее слово в этом объекте останавливает работу с
задачами целиком.
Ключей будет больше по мере роста проверок; неизвестный ключ `docs.py`
игнорирует, отсутствующий — считает «проверка неприменима» и говорит об этом
строкой, а не молчит.
@@ -1,183 +0,0 @@
# Журнал версий канона
Одна запись на версию. Проект знает свою версию из `docs/.pm.json`; `canon
upgrade` идёт по записям снизу вверх от версии проекта до текущей и делает то,
что в них названо.
Правило записи: **что добавилось, что переехало, что удалено, что сделать
проекту**. Без последнего пункта запись бесполезна — по ней и работает
`upgrade`.
Версия — целое число. Обратной совместимости у канона нет: есть «приведён» и «не
приведён».
---
## Версия 3 — 2026-08-04
Роадмап стал **состоянием проекта**, а не очередью работ: цель — возможность
приложения, задача — шаг к ней, достигнутое из роадмапа не исчезает. Плюс род
работы, раздел «Затрагивает» и новое умолчание профиля ревью. Раскладка меняется
в одном файле, но переименование и смена секций тянут за собой ссылки, поэтому
шаги делаются одним заходом.
**Что добавилось:**
1. **Род работы** — тег `kind:<род>` в мете задачи, словарь закрыт:
`feature` | `fix` | `chore` | `research`. Обязателен у задачи, у цели
запрещён. `sprint take` без него отказывает, `check` о пропаже напоминает
замечанием. Определение — [canon.md](canon.md), раздел `tasks/`; смысл и
причина, почему тегом, — в SKILL.md скилла `tasks`, раздел «Род работы».
2. **Раздел «Затрагивает»** в теле задачи — перечень границ, которых изменение
касается (эндпоинт, таблица и миграция, формат на диске, публичный тип). Как
и критерии приёмки, требуется к взятию в спринт, а не к заведению.
3. **Секции роадмапа** — четыре вместо двух и **канонические**, в отличие от
секций беклога: `Готово` (достигнутые цели строкой с датой, без ссылки на
файл), `Запланировано` (очередь значима), `Направления` (очереди нет),
`Разработка` (инструмент и процесс, не возможности приложения). Английский
вариант — `Done` | `Planned` | `Directions` | `Tooling`, один язык на весь
индекс. Переименованию проектом не подлежат: у каждой свой смысл, и в первую
пишет сам `close`; `tasks.py check` проверяет состав.
4. **Форма заголовка записи** — по типу: задача отвечает на «что нужно сделать»
и пишется глаголом в неопределённой форме («Не отбрасывать молча лишние
символы»), цель — на «что приложение будет уметь», идея просто называет, о
чём она. `check` считает заголовки не в форме действия и печатает число в
блоке здоровья. Годность формулировки — не машине: её смотрит новый агент
`task-form` (форма записи, только чтение), а язык текста — `doc-wording`.
5. **Заголовки секций — с прописной, после заголовка пустая строка**, во всех
индексах. Написание канонических секций и отбивку правит `check --fix`; он
же сводит написание секции в мете файла с заголовком индекса.
6. **Язык проектных текстов** — [language.md](language.md), общий дом для
документов канона, задач, решений ADR и записок разведки: информационный
стиль (глагол вместо отглагольного существительного, активный залог, факт
вместо оценки, стоп-слова, параллельность), таблицы англицизмов и жаргона и
то, что из стиля отброшено намеренно. Проектных файлов не добавляет и
раскладку не меняет — это правила письма, а не новый слот.
7. **Умолчание профиля ревью сменилось** — это не раскладка, но проектный текст
под него уже написан. `standard` стал рабочим умолчанием: миграция схемы,
публичный контракт и инвариант ступень больше **не** поднимают, `wide`
означает новое понятие или структурную единицу. Подраздел «Триггеры профиля»
в `docs/review.md` остаётся на месте, но его содержимое надо перечитать.
**Что переехало:** `docs/tasks/PLAN.md``docs/tasks/ROADMAP.md`; достигнутая
цель — из небытия в секцию `Готово`: `close <цель> --implemented` удаляет файл, но
**оставляет строку с датой**. Прежде роадмап отвечал только «что осталось», и
половину его вопроса вели прозой руками. Вместе с
файлом переименован ключ конфига `tasks.plan``tasks.roadmap` и токены
команд: `--index plan``--index roadmap`, `init --plan-sections`
`--roadmap-sections`, `init --plan``--roadmap`. Старый ключ в
`docs/.pm.json` не игнорируется молча — `tasks.py` останавливается и называет
переименование.
**Что удалено:** тип `[epic]`. Он был зонтиком между целью и задачами; зонтиком
стала цель, а слишком крупный шаг дробится на шаги помельче под ней. Ноль
употреблений на 97 записей двух живых проектов.
**Что сделать проекту:**
1. `git mv docs/tasks/PLAN.md docs/tasks/ROADMAP.md`.
2. Починить ссылки на прежнее имя: `grep -rn 'PLAN\.md' docs/ CLAUDE.md`
заголовок самого файла («# План» → «# Роадмап»), строка в `docs/tasks/BACKLOG.md`,
упоминания в `docs/passport.md` и в телах задач.
3. `docs/.pm.json`: ключ `tasks.plan`, если он там был, — в `tasks.roadmap`.
4. Проставить род работы живым задачам: `python3 tasks.py check --dir docs/tasks`
перечислит те, у кого его нет. Задним числом весь беклог не переоформляется —
род нужен к взятию, так что порядок такой: сперва то, что берётся в ближайший
спринт, остальное по ходу переоценки.
5. Дописать раздел «Затрагивает» — тем же порядком и по той же причине: сперва
набор спринта, остальное по мере того, как задача попадает в работу.
6. Перечитать «Триггеры профиля» в `docs/review.md`: строки вида «миграция →
`deep`» теперь дублируют умолчание с обратным знаком. Оставить там только то,
что для этого проекта считается **новым понятием** и **правилом
идентичности**, — и убрать остальное, иначе проект возвращает себе прежнюю
частоту полного набора уточнением.
7. Переименовать секции роадмапа: `порядок``Запланировано`, `темы`
`Направления`; завести `Готово` **первой** и `Разработка` последней.
Прозаические разделы вроде «Что уже пройдено», которые велись руками,
разложить: звенья — строками в `Готово` (дата, слаг, что стало возможно),
обоснование очереди оставить прозой в `Запланировано`. Любой `##` в индексе
проверка считает секцией, и теперь `check` называет чужую секцию ошибкой.
8. Переформулировать цели ответом на **«что приложение будет уметь»**: не
«Работа со слиянием», а «Исход слияния не зависит от порядка доставки».
Свойство поведения — законная цель. Цель, которая не про приложение
(процесс, инструмент), переезжает в `Разработка`.
9. `[epic]`, если он в проекте заводился: это либо цель, либо набор задач под
общей целью. `check` назовёт его неизвестным типом.
10. Прогнать `python3 tasks.py check --dir docs/tasks --fix`: он поднимет
написание канонических секций, поставит отбивку после заголовков и сведёт
секцию в мете файлов с заголовками индексов. Секции беклога проект
переименовывает сам — их имена он выбирал, и трогать их скрипт не вправе.
11. Переписать заголовки задач в форму действия — по мере того, как задача
попадает в работу, а не «заодно»: `check` печатает их число, а `task-form`
предложит формулировки на замену пачкой.
12. Прочитать [language.md](language.md) — и **ничего не переписывать задним
числом**. Правила языка применяются к тому, что пишется и правится сейчас;
сплошная вычитка старых документов стоит дороже, чем даёт.
13. `docs/.pm.json`: `"canon": 3`.
## Версия 2 — 2026-08-03
Шапка записи ADR — мета-блоком общей формы, и у статуса появился объявленный
дом. Раскладка не менялась: правка касается одного шаблона.
**Что добавилось:** поле `- **Статус:**` в шапке `docs/adr/template.md`
`заменено на ADR-…` либо `устарело`, у активной записи поля нет. Правило
«старая запись получает статус» было и раньше ([canon.md](canon.md), `adr/`),
но места под него шаблон не отводил: каждая запись изобретала своё, а колонка
«Статус» таблицы `adr/README.md` брала его оттуда, где он у каждого свой.
**Что переехало:** поля `Дата` и `Источник` в шаблоне стали жирными
(`- **Дата:**`, `- **Источник:**`) — та же форма, что у меты задачи и у записи
журнала дефектов: поле на строку, имя жирным.
**Что удалено:** ничего.
**Что сделать проекту:**
1. Привести `docs/adr/template.md` к скелету версии 2
([skeletons.md](skeletons.md), раздел `docs/adr/template.md`).
2. В существующих записях `docs/adr/ADR-*.md`: жирным поля шапки; если статус
записан прозой или заголовком — перенести его полем `- **Статус:**` в шапку
и сверить с колонкой «Статус» таблицы в `docs/adr/README.md`.
3. `docs/.pm.json`: `"canon": 2`.
## Версия 1 — 2026-08-03
Первая версия. Проект любой прежней раскладки приводится к ней скиллом `canon`
в режиме `adopt`, а не `upgrade`.
**Что вводится:** раскладка целиком — см. [canon.md](canon.md).
**Что сделать проекту, который приходит из свободной раскладки:**
1. `docs/.pm.json` с `{"canon": 1}` и путём миграций, если БД есть.
2. Скелет канона целиком; незаполненное — одной честной строкой.
3. `docs/specs/` разобрать: поведение — в `openspec/specs/`, обзор — в
`docs/architecture.md`, знание о чужих системах — в `docs/research/`.
Дубли capability удалить, сверив поимённо.
4. `docs/plan.md``docs/tasks/PLAN.md`, шаги плана — целями в «порядок».
5. `BRIEF.md``docs/passport.md`.
6. `docs/backlog/``docs/tasks/`.
7. `docs/review-journal.md` или `docs/review/journal.md``docs/review.md`,
плюс раздел настройки конвейера.
8. `docs/drafts/` растворить: идея → задача `[idea]`, намеренный отказ → ADR,
порядок работ → `PLAN.md`.
9. `docs/review-brief.md`, если заводился, удалить: его разделы разошлись по
документам канона.
10. `conventions.md``conventions/`, `local-research.md``research/`.
11. Завести `docs/security.md` с периметром первой строкой и `docs/adr/`.
12. В `CLAUDE.md`: severity рядом с каждым инвариантом; семантика гейта (чем
краснеет безусловно, где логи, чего в нём нет и кто тогда гоняет дорогое);
**имя основной ветки**; запреты с путями; где `testdata` и куда писать
временное; **что считается необратимым**; общий станок; ориентир по размеру
спринта. Убрать раздел «Процесс», если он пересказывает пайплайн.
13. В `openspec/config.yaml` оставить только нужды генерации и ссылки.
14. Добавить шаг `docs.py check` в гейт проекта.
**Копии правил в шаблонах, которые версия 1 уносит в проект** — их правка в
каноне обязана появляться здесь отдельной версией:
| Что копируется | Дом определения |
| --- | --- |
| форма записи журнала дефектов в `docs/review.md` | `av-dev-pipeline/skills/review-pipeline/references/review-journal.md` |
| правило заведения ADR в `docs/adr/README.md` | [canon.md](canon.md), раздел `adr/` |
@@ -1,176 +0,0 @@
# Язык проектных текстов
Правила для всего, что пишется словами: задачи и цели, документы канона,
решения ADR, записки разведки, сообщения коммитов. Не для кода и не для
сообщений программы пользователю — там свои конвенции проекта.
Основа — **информационный стиль** Максима Ильяхова ([учебник
бюро](https://bureau.ru/projects/book-text/), книга «Пиши, сокращай»). Он
написан для рекламы, статей и писем, поэтому взят не целиком: ниже сказано, что
взято и что отброшено намеренно.
## Зачем он здесь
Проектный текст читают в двух положениях, и оба неудобные: **выбирают, брать ли
задачу**, глядя в строку индекса и один экран тела; и **возвращаются через
квартал**, не помня контекста. Оба положения наказывают одно и то же — слова, не
несущие сведений. Информационный стиль ровно про это, и его польза здесь не
эстетическая: текст, из которого нельзя достать факт, заставляет открывать код,
а это и есть цена, которой мы избегаем.
## Что взято
**Полезное действие.** У каждого текста есть вопрос, на который он отвечает, и
читатель, который этот вопрос задаёт. Не отвечает — не пишется. У задачи это
«зачем она нужна» и «что станет наблюдаемо иначе»; у документа канона — его
собственный вопрос («что это за система», «как сложено», «почему так решили»).
Текст, который не отвечает ни на чей вопрос, сокращается до нуля — это законный
исход правки.
**Глагол вместо отглагольного существительного, действие вместо состояния.**
«Обработчик не проверяет владельца», а не «проверка владельца не
осуществляется»; «сопоставляет по имени», а не «осуществляет сопоставление по
имени». Отглагольное существительное прячет того, кто действует, — а в
техническом тексте именно он и важен.
**Активный залог.** «Скрипт переписывает индекс», а не «индекс переписывается
скриптом». Страдательный залог остаётся там, где деятель неизвестен или
неважен, и это не поблажка: «файл удаляется» верно, когда удаляет любая из трёх
команд.
**Конкретика вместо оценок.** Факты, имена, цифры: «время ответа доходит до
800 мс», а не «работает медленно»; «тело 40 МиБ держит блокировку 5 секунд», а
не «большие тела тормозят». Оценка допустима, когда за ней в той же фразе стоит
факт. Без факта оценка — не сведение, а настроение.
**Стоп-слова.** Убирается то, что можно убрать без потери смысла:
| Что | Примеры |
| --- | --- |
| канцелярит | является, осуществляется, в целях, в рамках, данный, вышеуказанный, необходимо отметить |
| вводные-паразиты | в общем, как известно, стоит отметить, не секрет, что |
| усилители | очень, крайне, достаточно, абсолютно, максимально, полностью |
| синонимы одного качества | «понятный и простой», «быстрый и производительный» |
| неопределённое | какой-то, некоторый, соответствующий, определённый |
Проверка одна: **вычеркни слово. Смысл изменился — оставляй.**
**Одна мысль — одно предложение.** Предложение, в котором два независимых
утверждения, делится. Придаточное, которое можно вынести в отдельную фразу,
выносится.
Исключение — **поля, которым формат отвёл одно предложение**. «Зачем» в мете
задачи именно такое: оно повторяется строкой индекса, и второе предложение там
просто не поместится. Такое поле либо укладывается в одну фразу, либо
сокращается, но не делится.
**Параллельность.** Однородное пишется одинаково: пункты списка — одной
грамматической формой, разделы одного вида — одним порядком, заголовки одного
уровня — одним типом фразы. Разнобой читатель принимает за разницу по существу и
ищет её.
**Заголовок работает.** Заголовок называет содержание раздела, а не тему
вообще: «Что проверяет `check`», а не «О проверках». Заголовков ставится
столько, чтобы длинный текст можно было просматривать, а не только читать
подряд.
## Что отброшено намеренно
Инфостиль написан для текстов, где читателя надо удержать. Проектный текст
читают потому, что надо, и держать его нечем. Отсюда три расхождения:
- **Парцелляция и рубленые фразы — нет.** Приём «Коротко. Ещё короче. Вот так»
ломает причинную связь, а в решении и в задаче ценность именно в ней:
«поэтому», «иначе», «раз так» несут смысл и остаются.
- **Не всякое вводное — мусор.** «Если», «иначе», «при таком-то условии»,
«в отличие от» — это условия и противопоставления, то есть сведения. Режутся
вводные, которые не меняют смысл предложения.
- **Скобки и точка с запятой остаются.** В технической записи скобки несут
уточнение — имя команды, единицы, слаг, — и запрет на них удлинил бы текст, а
не сократил. Запрет на многоточие принимаем: в проектном тексте оно значит
«дописать позже», и такой текст лучше не публиковать.
И общее: инфостиль призывает «снять корону с себя и надеть на читателя». Здесь
читатель — **ты сам через квартал** и тот, кто возьмёт задачу. Писать для них
значит называть состояние и остаток, а не пересказывать, как было интересно
разбираться.
## Англицизмы
Англицизм-калька заменяется, когда у него есть естественный русский аналог.
<!-- дом: язык-англицизмы -->
| Калька | Русский аналог |
| --- | --- |
| флоу | поток, процесс, сценарий |
| фикс, зафиксить | исправление, исправить, починить |
| чекать | проверять |
| апрув, заапрувить | согласование, согласовать |
| best-effort | по возможности |
| кейс | случай, сценарий |
| перформанс | производительность |
| матчинг, смэтчить | сопоставление, сопоставить |
| зарелизить | выпустить, выложить |
| отрефакторить | переписать, разделить, убрать второй путь |
Насильно не переводится то, что является **именем вещи**: термины технологий и
протоколов (`SQL`, `API`, `CSV`, `N+1`, `IDOR`), имена классов, методов, полей,
таблиц и команд, слаг, а также термин, у которого нет точного русского
эквивалента и который в команде уже прижился.
Цель — простой и точный текст, а не пуризм. Русский аналог звучит коряво или
искажает смысл — остаётся термин.
<!-- /дом: язык-англицизмы -->
## Жаргон и метафоры
Система не описывается внутренними метафорами и образными ярлыками: автору они
понятны, читателю — нет. Вещь называется прямо.
<!-- дом: язык-жаргон -->
| Метафора-жаргон | Прямо |
| --- | --- |
| рычаг (кэша, отбора) | условие отбора, параметр |
| навешен не на тот счётчик | завязан не на тот счётчик |
| переширокий матчинг по имени | слишком грубое сопоставление по имени, слишком много слабых совпадений |
| костыль | временное решение, обходной путь — и в чём именно |
| просело, отвалилось | стало медленнее на столько-то, перестало отвечать |
Проверка: **фраза требует, чтобы читатель додумал образ, — заменяется буквальным
описанием того, что происходит.**
<!-- /дом: язык-жаргон -->
## Термин, которого нет в проекте
Термин, не встречающийся ни в паспорте, ни в архитектуре, ни в конвенциях,
**вводится одной строкой или не употребляется**. Свой словарь у отдельной записи
— самый дешёвый способ сделать беклог нечитаемым для того, кто вернётся к нему
через квартал.
Заменять незнакомый термин догадкой нельзя: догадка о предметной области дороже
непонятного слова, потому что выглядит понятной.
## Порог правки
<!-- дом: порог-правки -->
**Правка без нарушенного правила не делается.** Текст, переписанный «чтобы
звучало лучше», обесценивает список замечаний: когда половина из них вкусовая,
перестают читать весь список, и вместе с ним пропадают настоящие находки.
Сомневаешься — не правь. Формулировка, которая просто **не твоя**, — не находка.
**Систематичность нарушения — не довод в его пользу.** Одна и та же ошибка в
пяти файлах не становится «принятым стилем»: чаще это значит, что правило не
применялось вовсе, — и находка тем важнее. «Так сделано везде» годится как
основание для **одной находки на весь набор** («правило N нарушено в пяти
записях, перечень: …»), но не как основание промолчать. Принятым считается
только то, что назвал зовущий или что записано в конвенциях проекта.
<!-- /дом: порог-правки -->
И обратное: язык правится **по ходу той операции, которая записи касается**.
Беклог не переписывают ради языка.
-441
View File
@@ -1,441 +0,0 @@
#!/usr/bin/env python3
"""Проверка раскладки документов проекта против канона av-dev.
Определение канона — references/canon.md рядом со скриптом. Здесь только
механизируемая часть: пути, лишние файлы, битые ссылки, версия, плейсхолдеры,
маркеры долга и две сверки с кодом. Смысловые дубли и оставшееся в архитектуре
поведение судит агент — скрипт об этом говорит вслух в конце отчёта.
Коды выхода — тот же словарь, что у tasks.py:
0 сошлось
1 дрейф раскладки (рабочая ситуация, чинится)
2 ошибка употребления
3 окружение: не тот каталог, битый конфиг
4 внутренний сбой
"""
from __future__ import annotations
import argparse
import json
import re
import subprocess
import sys
from dataclasses import dataclass, field
from pathlib import Path
from typing import NoReturn
CANON_VERSION = 3
OK, DRIFT, USAGE, ENV, INTERNAL = 0, 1, 2, 3, 4
# --- Раскладка канона -------------------------------------------------------
# Обязательные файлы: путь → на какой вопрос отвечает (для внятного отказа).
REQUIRED = {
"CLAUDE.md": "памятка агенту: инварианты с severity, команды, семантика гейта",
"docs/.pm.json": "версия канона и пути, нужные проверкам",
"docs/passport.md": "зачем и для кого, чем НЕ является",
"docs/architecture.md": "как сложено — обзор, окружение, эксплуатация",
"docs/security.md": "периметр, недоверенный вход, что вне модели",
"docs/review.md": "настройка конвейера + журнал дефектов",
"docs/conventions/README.md": "индекс конвенций, правило промоута, что механизировано",
"docs/research/README.md": "как снималось, индекс наблюдений",
"docs/adr/README.md": "индекс записей, статусы, правило замены",
"docs/adr/template.md": "шаблон записи ADR",
}
# Обязателен только при условии: путь → (ключ .pm.json, пояснение).
CONDITIONAL = {
"docs/database.md": ("migrations", "схема хранилища и настройки"),
}
# Что вообще разрешено лежать в docs/ верхним уровнем.
ALLOWED_FILES = {
".pm.json",
"passport.md",
"architecture.md",
"database.md",
"security.md",
"review.md",
}
ALLOWED_DIRS = {"conventions", "research", "adr", "tasks"}
# Слоты, которых в каноне нет, — с адресом, куда уезжает содержимое.
RETIRED = {
"review-brief.md": "документы канона и есть бриф; остаток — в review.md",
"review-journal.md": "→ docs/review.md",
"plan.md": "→ docs/tasks/ROADMAP.md",
"conventions.md": "→ docs/conventions/",
"local-research.md": "→ docs/research/",
"research.md": "→ docs/research/",
"specs": "поведение → openspec/specs/, обзор → docs/architecture.md",
"drafts": "идея → задача [idea], отказ → ADR, порядок → ROADMAP.md",
"backlog": "→ docs/tasks/",
"review": "→ docs/review.md",
}
DEBT_MARKER = re.compile(r"<!--\s*канон:\s*(.+?)\s*-->")
PLACEHOLDER = re.compile(r"<!--\s*заполнить:\s*(.+?)\s*-->")
MD_LINK = re.compile(r"\[[^\]]*\]\(\s*<?([^)>\s]+)>?(?:\s+[\"'(][^)]*)?\)")
FENCE = re.compile(r"^\s*(```|~~~)")
INLINE_CODE = re.compile(r"`[^`\n]*`")
def strip_code(text: str) -> str:
"""Выкинуть блоки кода и вставки в обратных кавычках.
Путь в примере или в шаблоне — не ссылка, и краснеть на нём значит краснеть
на каждом образце документа. Инлайн-код тоже: `[docs/backlog](docs/tasks/…)`
в тексте про подписи ссылок — иллюстрация, а не ссылка."""
out, inside = [], False
for line in text.splitlines():
if FENCE.match(line):
inside = not inside
continue
out.append("" if inside else INLINE_CODE.sub("", line))
return "\n".join(out)
@dataclass
class Report:
errors: list[str] = field(default_factory=list)
notes: list[str] = field(default_factory=list)
debts: list[str] = field(default_factory=list)
skipped: list[str] = field(default_factory=list)
def error(self, msg: str) -> None:
self.errors.append(msg)
def note(self, msg: str) -> None:
self.notes.append(msg)
def debt(self, msg: str) -> None:
self.debts.append(msg)
def skip(self, msg: str) -> None:
self.skipped.append(msg)
def fail(code: int, msg: str) -> NoReturn:
print(f"ОТКАЗ: {msg}", file=sys.stderr)
sys.exit(code)
def read_config(root: Path, rep: Report) -> dict:
path = root / "docs" / ".pm.json"
if not path.exists():
return {}
try:
data = json.loads(path.read_text(encoding="utf-8"))
except json.JSONDecodeError as exc:
fail(ENV, f"docs/.pm.json не разбирается: {exc}")
if not isinstance(data, dict):
fail(ENV, "docs/.pm.json должен быть объектом")
return data
# --- Проверки ---------------------------------------------------------------
def check_version(root: Path, cfg: dict, rep: Report) -> None:
if not (root / "docs" / ".pm.json").exists():
return # об отсутствии файла скажет check_required, второй раз не нужно
if "canon" not in cfg:
rep.error("в docs/.pm.json нет ключа canon — версия канона не объявлена")
return
got = cfg["canon"]
if not isinstance(got, int):
rep.error(f"canon в docs/.pm.json должен быть целым числом, а не {got!r}")
return
if got < CANON_VERSION:
rep.error(
f"проект приведён к канону версии {got}, текущая — {CANON_VERSION}: "
f"нужен canon upgrade"
)
elif got > CANON_VERSION:
rep.error(
f"проект приведён к канону версии {got}, а скрипт знает {CANON_VERSION}: "
f"устарел плагин, обнови маркетплейс"
)
def check_required(root: Path, cfg: dict, rep: Report) -> None:
for rel, what in REQUIRED.items():
if not (root / rel).exists():
rep.error(f"нет {rel}{what}")
for rel, (key, what) in CONDITIONAL.items():
if key in cfg and not (root / rel).exists():
rep.error(f"нет {rel}{what} (обязателен: в .pm.json объявлен {key})")
elif key not in cfg and not (root / rel).exists():
rep.skip(f"{rel} — в .pm.json нет ключа {key}, проверка неприменима")
def check_stray(root: Path, rep: Report) -> None:
docs = root / "docs"
if not docs.is_dir():
rep.error("нет каталога docs/")
return
for entry in sorted(docs.iterdir()):
name = entry.name
if name in RETIRED:
rep.error(f"docs/{name} — слота нет в каноне: {RETIRED[name]}")
continue
if entry.is_dir():
if name not in ALLOWED_DIRS:
rep.error(f"docs/{name}/ — каталог вне канона")
elif name not in ALLOWED_FILES:
rep.error(f"docs/{name} — файл вне канона")
def canon_docs(root: Path) -> list[Path]:
"""Документы канона. Каталог задач ведёт tasks.py; упразднённые каталоги
уже названы отдельной строкой, и их внутренние ссылки не наша забота —
они переезжают целиком."""
out = []
docs = root / "docs"
skip = {"tasks"} | {name for name in RETIRED if not name.endswith(".md")}
if docs.is_dir():
for path in sorted(docs.rglob("*.md")):
head = path.relative_to(docs).parts[0]
if head in skip or head in RETIRED:
continue
out.append(path)
claude = root / "CLAUDE.md"
if claude.exists():
out.append(claude)
return out
def check_links(root: Path, rep: Report) -> None:
for path in canon_docs(root):
try:
text = path.read_text(encoding="utf-8")
except OSError as exc:
rep.error(f"{path.relative_to(root)} не читается: {exc}")
continue
for target in MD_LINK.findall(strip_code(text)):
target = target.strip()
if not target or target.startswith(("http://", "https://", "#", "mailto:")):
continue
clean = target.split("#", 1)[0]
if not clean:
continue
if (path.parent / clean).exists():
continue
rep.error(f"{path.relative_to(root)}: битая ссылка на {target}")
def check_placeholders_and_debt(root: Path, rep: Report) -> None:
for path in canon_docs(root):
text = strip_code(path.read_text(encoding="utf-8", errors="replace"))
rel = path.relative_to(root)
for what in PLACEHOLDER.findall(text):
# Замечание, а не дрейф: незаполненный канон — объявленное переходное
# состояние, и краснеть на нём значит требовать выдумать содержание.
rep.note(f"{rel}: плейсхолдер шаблона не заполнен — {what}")
for what in DEBT_MARKER.findall(text):
rep.debt(f"{rel}: {what}")
def check_capabilities(root: Path, rep: Report) -> None:
specs = root / "openspec" / "specs"
arch = root / "docs" / "architecture.md"
if not specs.is_dir():
rep.skip("openspec/specs/ нет — сверка capability с архитектурой неприменима")
return
if not arch.exists():
rep.skip(
"docs/architecture.md нет — capability не сверены с обзором "
"(об отсутствии файла сказано отдельной строкой)"
)
return
text = arch.read_text(encoding="utf-8", errors="replace")
for d in sorted(specs.iterdir()):
if not d.is_dir():
continue
name = d.name
# Засчитываем только явное упоминание: ссылку на спеку или имя в обратных
# кавычках. Голая подстрока совпадает с именем пакета или CLI-команды и
# даёт ложное «упомянуто» — то есть проверку, проходящую не по той причине.
explicit = f"openspec/specs/{name}" in text or f"`{name}`" in text
loose = re.search(rf"\b{re.escape(name)}\b", text) is not None
if explicit:
continue
if loose:
rep.note(
f"capability {name}: в docs/architecture.md есть слово «{name}», но "
f"нет ни ссылки на openspec/specs/{name}, ни имени в обратных "
f"кавычках — проверь, это про capability или про пакет"
)
else:
rep.error(
f"capability {name} есть в openspec/specs/, но не упомянута в "
f"docs/architecture.md — обзор отстал от нормативных спек"
)
def changed_files(root: Path, base: str, rep: Report) -> list[str] | None:
"""Объединение закоммиченного, рабочего дерева и untracked.
Гейт гоняют ДО коммита, поэтому `base...HEAD` не видит ровно ту правку, ради
которой проверка и заводилась: миграция уже лежит в дереве, но ещё не в
истории. Пропущенная правка выглядела бы как зелёный шаг."""
cmds = [
["diff", "--name-only", base],
["ls-files", "--others", "--exclude-standard"],
]
seen: list[str] = []
for cmd in cmds:
try:
out = subprocess.run(
["git", "-C", str(root), *cmd],
capture_output=True,
text=True,
check=True,
)
except (subprocess.CalledProcessError, FileNotFoundError) as exc:
rep.skip(f"сверка миграций пропущена: git не отдал дифф ({exc})")
return None
seen.extend(line for line in out.stdout.splitlines() if line)
return sorted(set(seen))
def check_migrations(root: Path, cfg: dict, base: str | None, rep: Report) -> None:
migrations = cfg.get("migrations")
if not migrations:
rep.skip("в .pm.json нет ключа migrations — сверка со схемой неприменима")
return
if not base:
rep.skip("база диффа не названа (--base) — сверка миграций со схемой не гонялась")
return
changed = changed_files(root, base, rep)
if changed is None:
return
touched = [f for f in changed if f.startswith(migrations.rstrip("/") + "/")]
if not touched:
return
if "docs/database.md" not in changed:
rep.error(
f"миграции изменены ({len(touched)} файлов), а docs/database.md — нет: "
f"схема в документации отстала"
)
def check_tasks(root: Path, rep: Report) -> None:
tasks = root / "docs" / "tasks"
if not tasks.is_dir():
rep.error("нет docs/tasks/ — каталог задач часть канона")
return
script = Path(__file__).resolve().parents[2] / "tasks" / "scripts" / "tasks.py"
if not script.exists():
rep.skip(f"tasks.py не найден по пути {script} — согласованность задач не проверена")
return
# 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 'без сообщения'}"
)
# --- Отчёт ------------------------------------------------------------------
def report(rep: Report) -> int:
for msg in rep.errors:
print(f"ДРЕЙФ {msg}")
for msg in rep.notes:
print(f"ЗАМЕЧАНИЕ {msg}")
if rep.debts:
print(f"\nДОЛГ ({len(rep.debts)} маркеров, гейт от них не краснеет):")
for msg in rep.debts:
print(f" {msg}")
if rep.skipped:
print("\nНЕ ПРОВЕРЯЛОСЬ:")
for msg in rep.skipped:
print(f" {msg}")
print(
"\nМашина проверила раскладку, ссылки, версию и две сверки с кодом.\n"
"Смысловые дубли, оставшееся в architecture.md поведение и достаточность\n"
"честной строки в пустом слоте она не проверяет — это суждение агента."
)
if rep.errors:
print(f"\nИтог: дрейф, {len(rep.errors)} пунктов.")
return DRIFT
print("\nИтог: канон соблюдён в механизируемой части.")
return OK
def cmd_check(args: argparse.Namespace) -> int:
root = Path(args.dir).resolve()
if not root.is_dir():
fail(ENV, f"каталог {root} не найден")
if not (root / "docs").exists() and not (root / "CLAUDE.md").exists():
fail(ENV, f"{root} не похож на корень проекта: нет ни docs/, ни CLAUDE.md")
rep = Report()
cfg = read_config(root, rep)
check_version(root, cfg, rep)
check_required(root, cfg, rep)
check_stray(root, rep)
check_links(root, rep)
check_placeholders_and_debt(root, rep)
check_capabilities(root, rep)
check_migrations(root, cfg, args.base, rep)
check_tasks(root, rep)
return report(rep)
def cmd_version(args: argparse.Namespace) -> int:
root = Path(args.dir).resolve()
cfg = read_config(root, Report())
got = cfg.get("canon", "не объявлена")
print(f"канон скрипта: {CANON_VERSION}")
print(f"канон проекта: {got}")
return OK
def main() -> int:
parser = argparse.ArgumentParser(
prog="docs.py",
description="механическая проверка канона документов проекта",
)
sub = parser.add_subparsers(dest="cmd", required=True)
p_check = sub.add_parser("check", help="раскладка, ссылки, версия, сверки с кодом")
p_check.add_argument("--dir", default=".", help="корень проекта (по умолчанию текущий)")
p_check.add_argument("--base", default=None, help="база диффа для сверки миграций")
p_check.set_defaults(func=cmd_check)
p_ver = sub.add_parser("version", help="версия канона скрипта и проекта")
p_ver.add_argument("--dir", default=".", help="корень проекта")
p_ver.set_defaults(func=cmd_version)
args = parser.parse_args()
try:
return args.func(args)
except SystemExit:
raise
except Exception as exc: # noqa: BLE001 — последний рубеж, код 4 по словарю
print(f"ВНУТРЕННИЙ СБОЙ: {exc}", file=sys.stderr)
return INTERNAL
if __name__ == "__main__":
sys.exit(main())
-136
View File
@@ -1,136 +0,0 @@
---
name: docs
description: Вести содержимое документов канона по ходу разработки — синк после сделанной задачи с построчным отчётом по каждому документу, заведение ADR промоутом из архивного design.md, запись наблюдения в research, запись дефекта и настройки конвейера в review.md, чистка architecture.md от поведения с маркерами долга. Использовать, когда задача сделана и надо обновить документацию, когда просят завести ADR или записать решение, занести находку о внешних данных, записать проскочивший дефект, разгрузить разросшуюся архитектуру. Раскладку и соответствие канону проверяет скилл canon.
---
# Ведение содержимого канона
Скилл владеет **содержимым** документов канона; раскладкой владеет `canon`.
Определение канона и роли документов — [канон](../canon/references/canon.md),
здесь не пересказывается.
Главный вызывающий — **шаг синка документации в пайплайне задачи**. Пайплайн
живёт в другом плагине и зовёт этот скилл по имени; проект без пайплайна ведёт
документацию тем же скиллом вручную.
## Правило, из которого всё следует
**Принуждённое отрицание.** Синк обязан назвать **каждый** документ канона: либо
чем он обновлён, либо «не требуется, потому что…». Нетронутые группируются одной
строкой с общей причиной.
Причина, по которой правило именно такое, измерена: у ADR был список триггеров
прозой — и он дал **6 записей на 43 изменения**. Прозаический триггер, который
некому проверить, не срабатывает. Умолчание «не написал» становится отличимым от
«написал, что не требуется», только когда отрицание обязательно.
Это тот же приём, что «границы покрытия» в отчёте ревью и «пустое называется
пустым» в каноне.
## Чек-лист синка
Идёт сверху вниз; каждая строка попадает в доклад.
| Документ | Обновляется, когда | Проверка |
| --- | --- | --- |
| `openspec/specs/` | всегда при изменении поведения | вливает `opsx:archive` |
| `database.md` | тронуты миграции | `docs.py check --base` |
| `architecture.md` | новый компонент, граница, внешняя зависимость, изменилось окружение | `docs.py`: capability без упоминания |
| `adr/` | дорогой откат, намеренный отказ, пересмотр прежнего | нет — только этот чек-лист |
| `research/` | узнали новое о внешнем формате или данных | нет |
| `security.md` | новый недоверенный вход, токен, путь наружу, сдвиг периметра | нет |
| `conventions/` | находка принята и не специфична для одного места | промоут |
| `review.md` | дефект воспроизведён; сузили или расширили проверку | нет |
| `passport.md` | новый потребитель, сдвиг границы «чем не является» | нет |
| `CLAUDE.md` | изменился инвариант, гейт, запрет, необратимое | нет |
Пример доклада:
```
Синк документации:
- architecture.md — добавлен воркер свёртки, ссылка на capability reindex
- database.md — миграция 00006, таблица bucket
- adr/ — заведён ADR-2026-08-03-ochered-tablicej: отказ от внешней очереди
- research/ — новое о формате не узнано
- passport, security, conventions, review — не требуется: изменение внутреннее
```
## ADR — промоут, а не второе сочинение
Обоснование уже написано: `opsx:propose` кладёт `design.md` в каждый change, и
после архивации он лежит в `openspec/changes/archive/<id>/design.md` с разделами
`Context` / `Goals / Non-Goals` / `Decisions` / `Risks / Trade-offs`.
**ADR цитирует решение оттуда и ссылается на источник.** Не пересказывает и не
сочиняет заново.
**Триггер заведения, форма имени и правило замены — в
[каноне](../canon/references/canon.md), раздел `adr/`.** Здесь они не
повторяются: копия правила расходится с оригиналом на первой же смене версии
канона, а расходится незаметно.
Твоя часть — **применить триггер к этой задаче и сказать вслух, сработал он или
нет**. Строка «adr/ — не требуется: решение рутинное» и есть то, ради чего
чек-лист существует; её отсутствие неотличимо от «забыл посмотреть».
Порядок работы: открой архивный `design.md` change, найди в `Decisions` то, что
проходит триггер, процитируй решение и его причину, сошлись на источник, добавь
строку в индекс `docs/adr/README.md` сверху.
## Чистка `architecture.md`
Обзор не держит поведение — его нормативный дом `openspec/specs/`. **Форма
маркера долга и правило «гейт от них не краснеет» — в
[каноне](../canon/references/canon.md), раздел `architecture.md`.**
Разбирается порциями: раздел вычищается той задачей, которая его касается.
Содержимое не выбрасывается, а переезжает — требования в дельта-спеку change,
обоснование в ADR, обзор остаётся строкой со ссылкой на capability.
## Запись в `research/`
Наблюдение о внешнем мире: что реально шлёт источник, чем документация формата
расходится с практикой. **Требование провенанса и правило про расходящееся
число — в [каноне](../canon/references/canon.md), раздел `research/`.**
Твоя часть — заметить, что по ходу задачи узналось новое о внешних данных, и не
дать этому остаться в контексте. Признак: ты правил разбор, опираясь на то, чего
нет ни в одном документе.
## Запись в `review.md`
Файл держит два раздела с разными сроками жизни — журнал дефектов и настройку
конвейера. **Что в каком и в какой форме — в
[каноне](../canon/references/canon.md), раздел `review.md`**; подробности формы
записи и выбор адреса, куда она ведёт, — у конвейера ревью проекта (при
`av-dev-pipeline``Skill av-dev-pipeline:review-pipeline`, его
`references/review-journal.md`). **Конвейера в проекте нет** — пиши по форме из
скелета `review.md`, которую положил канон, и скажи в докладе, что подробностей
формы взять негде.
Твоя часть на синке: **дефект пишется сразу**, а не «потом, когда починим».
Со временем теряется не факт, а причина непоймания — единственное, ради чего
журнал есть. И решение о сужении проверок (перестали звать проход, понизили
профиль) обязано попасть в раздел настройки, а не остаться в отчёте ревью.
## Промоут в конвенции
Находка → конвенция → правило линтера → **удаление из прозы**. Процедура целиком
принадлежит конвейеру ревью проекта (при `av-dev-pipeline` — его
`references/promote.md`, читается через `Skill av-dev-pipeline:review-pipeline`);
роль каталога конвенций — в [каноне](../canon/references/canon.md). **Конвейера
нет** — три шага всё равно твои, просто без его процедуры: сформулируй правило,
поищи, чем оно механизируется, и вычеркни прозу, если механизировалось.
Твоя часть — **третий шаг, который пропускают чаще всего**: правило заработало,
а формулировка осталась в прозе, и проход продолжает проверять уже проверенное.
На синке это отдельная строка: «conventions/ — правило X механизировано,
формулировка удалена» либо «не требуется».
## Чего этот скилл не делает
- **Не проверяет раскладку** — это `canon`.
- **Не заводит недостающие документы** — их скелет кладёт `canon adopt` или
`init`.
- **Не сочиняет содержание.** Нечего записать — так и пишется, честной строкой.
- **Не переоформляет документы «заодно»**: правится то, чего коснулась работа.
-97
View File
@@ -1,97 +0,0 @@
---
name: init
description: "Завести новый проект — сессия вопросов и ответов по свободному описанию замысла, из которой рождается первичная документация по канону av-dev: паспорт, CLAUDE.md с инвариантами и командами, модель угроз с периметром, первые цели в роадмапе и скелет остальных документов. Использовать, когда начинают новый проект с нуля, когда есть только текст «что мне нужно и почему» и надо превратить его в рабочую документацию, когда просят провести стартовое интервью по брифу. Проект, где документация уже как-то ведётся, переводит скилл canon."
---
# Заведение нового проекта
Вход — свободный текст «что мне нужно и почему». Выход — канон документов, с
которого дальше работают все остальные скиллы.
**Определение канона — [канон](../canon/references/canon.md).** Читается до
первого вопроса: интервью идёт по слотам канона, а не по вкусу. Что класть в
каждый файл — [скелеты](../canon/references/skeletons.md); своей формы заглушки
не выдумывай, `docs.py` узнаёт только плейсхолдер оттуда.
## Что `init` физически не может произвести
В новом репозитории **нет кода**, а `architecture.md`, `database.md`,
`conventions/` и `research/` выводятся из него. Их сочинение на старте — это
проектирование вперёд реальности, и оно протухнет раньше первой задачи.
Поэтому `init` заполняет то, что человек знает **до первой строки кода**:
| Заполняется | Остаётся скелетом с честной строкой |
| --- | --- |
| `passport.md` | `architecture.md` |
| `CLAUDE.md` | `database.md` |
| `security.md` | `conventions/` |
| `docs/tasks/ROADMAP.md` — первые цели | `research/`, `adr/` |
| `docs/.pm.json` | `review.md` — журнал пуст, настройка появится с первым ревью |
Честная строка информативна, а не «TBD»: «архитектуры пока нет: кода нет,
заводится первой задачей». Проход читает её как факт.
## Порядок интервью — зависимость, а не удобство
Каждый блок опирается на ответ предыдущего; переставлять нельзя.
1. **Цель и потребители.** Ради чего это; кто пользуется — список закрытый, и
он определяет, что считать нужным, а что интересным.
2. **Чем это НЕ является и мера успеха.** Граница домена — критерий, по
которому архитектурный проход потом судит о переносе понятия. Мера — по чему
поймём, что удалось.
3. **Периметр и недоверенный вход.** Открыт наружу или контур доверенный; что
приходит извне и каким каналом; что чувствительнее чего. Контур ещё не
развёрнут — назови **оба** периметра, целевой и сегодняшний.
4. **Стек, хранилище, необратимое.** Чем пишем и почему; где данные; что в этом
проекте нельзя откатить — деплой, выкладка наружу, перезапись данных.
5. **Чем краснеет гейт.** Какие проверки обязательны; что красит безусловно;
чего в гейте намеренно не будет и кто тогда это гоняет.
6. **Первые цели.** Возможности приложения, а не задачи: три-пять целей в
`Запланировано`, каждая — ответ на «что приложение будет уметь», с
обоснованием очереди прозой.
### Как вести
- **Не больше трёх вопросов за итерацию** (`AskUserQuestion`), рекомендация
первым вариантом. Между итерациями применяй уже решённое.
- **Сперва вычитай ответы из брифа.** Вопрос, ответ на который в тексте уже
есть, задавать не надо — покажи своё прочтение и спроси, верно ли.
- **Не выдумывай четыре вещи:** периметр, что необратимо, измеренные числа и
адресата дорогой проверки. Их из замысла не вывести. Не сказано — пиши
«неизвестно» с пометкой, что ждёт ответа.
- **Развилка замысла — человеку, механика — сама.** Имена файлов, слаги, порядок
строк не выносятся.
## Порядок работы
1. Прочитай бриф целиком. Выпиши, на какие блоки интервью ответ уже есть.
2. Проведи интервью итерациями по ≤3 вопроса.
3. Заведи `docs/.pm.json` с текущей версией канона.
4. Напиши заполняемые документы. **Бриф переезжает в `passport.md`** и
отдельным файлом не остаётся: два дома для одного замысла разойдутся на
первом же уточнении.
5. Заведи скелет остальных по [скелетам](../canon/references/skeletons.md) —
каждый с честной строкой.
6. Каталог задач и первые цели — **вызови скилл `av-dev-pm:tasks`**: он владеет
форматом целей и задач.
7. `docs.py check` из скилла `canon` — до отсутствия дрейфа. Замечания о
незаполненных плейсхолдерах остаются: их закрывает не `init`, а работа.
8. Покажи человеку, что получилось, и **отдельным списком** — что выведено из
брифа, что предположено, что осталось неизвестным. Правят по этим строкам.
## Что дальше
- Содержимое канона по ходу разработки ведёт скилл `docs`.
- Раскладку проверяет `canon check`.
- Первую задачу берёт пайплайн проекта; `architecture.md` и `conventions/`
наполняются его шагом синка, а не заранее.
## Чего этот скилл не делает
- **Не проектирует систему.** Архитектура выводится из кода, а не наоборот.
- **Не пишет код** и не заводит сборку.
- **Не переводит существующий проект** — это `canon adopt`. Признак: в
репозитории уже есть документация или беклог в какой-то раскладке.
- **Не решает за человека**, что важно: цель, границы и периметр — его ответы.
-262
View File
@@ -1,262 +0,0 @@
---
name: session
description: "Ритуал между спринтами и ведение самого спринта: разбор накопившихся вопросов, разбор прошедшего спринта про процесс, переоценка задач порциями, выбор цели и набор нового спринта с заморозкой. Плюс правила по ходу спринта — что врывается в замороженный набор, чем вопрос отличается от блокера, когда задача выходит из спринта, что считается сделанным и что идёт в доклад. Использовать, когда просят закрыть или начать спринт, собрать набор, разобрать вопросы, провести груминг/переоценку/ретроспективу, решить «что делать дальше» или доложить итоги. Формат и содержимое задач — скилл tasks."
---
# Сессия между спринтами
Работа идёт спринтами: **набор задач под одну цель, замороженный до конца
спринта**. Между спринтами — одна сессия из четырёх шагов. Этот скилл владеет
**ритуалом**: как сессия проводится и как спринт ведётся. Форматом и содержимым
задач владеет скилл `tasks`, выполнением задачи — пайплайн проекта.
## Почему не Scrum
Терминология близка — спринт, груминг, определение готовности, ретроспектива, —
и это удобно: не нужно изобретать слова. Но добрая половина Scrum существует
ради синхронизации людей, которых здесь нет.
**Не берём:** тайм-бокс (спринт ограничен объёмом, а не временем), velocity и
оценки в очках, ежедневный стендап (стендап — это и есть диалог), планирование
отдельно от груминга (владелец беклога один), роль скрам-мастера.
**Берём:** цель спринта, заморозку набора, определение готовности, груминг —
каждое потому, что снимает решение, которое иначе принимается заново каждый раз.
**Ретроспективу берём содержанием, но не отдельным ритуалом:** она шаг той же
сессии. Процесс личный, синхронизировать некого, а отдельная встреча ради трёх
вопросов — та самая плата ритуалом без выгоды.
## Роли
**Человек** выбирает цель спринта, разбирает вопросы, держит право на
необратимое и на истину в самих данных.
**Агент — оркестрация.** Он собирает набор под названную цель, ставит задачи,
принимает отчёты и докладывает. Кто именно делает задачу — исполнитель, сабагент,
пайплайн — дело проекта; сессия про это не знает и знать не должна.
## Единицы
- **Цель** — то, ради чего набирается спринт. Файл `[goal]`, перечисленный в
`ROADMAP.md`. Цель постоянна: живёт, пока живёт направление.
- **Задача** — то, что мерджится целиком и даёт видимую пользу.
- **Вопрос** — решение человека. Не останавливает начатую работу, но **блокирует
взятие** задачи в спринт. Живёт внутри файла задачи разделом «Вопросы» и тегом
`question`.
- **Блокер** — состояние, когда спринт не может продолжаться **ни одной**
задачей.
- **Спринт** — набор задач под одну цель, замороженный до его конца.
## Вопрос, блокер, необратимое
| | Что это | Когда спрашиваем | Что останавливает |
| --- | --- | --- | --- |
| **Вопрос** | решение человека | на сессии, пачкой | взятие задачи в спринт |
| **Блокер** | спринт не может продолжаться ни одной задачей | немедленно | всё |
Право на **необратимое** — третье и отдельное: что именно необратимо, называет
`CLAUDE.md` проекта, и спрашивается оно всегда, независимо от спринта.
**Блокер определяется исходом, а не одновременностью.** Встали разом или
высыпались из спринта по одной — если продолжать нечем, это блокер: спринт
распускается (`sprint close --dissolve --reason …`), человек спрашивается
немедленно. Иначе спринт, из которого задачи вышли поштучно, выглядел бы штатно
завершённым, а вопросы тихо ждали бы сессии.
**Отличать вопрос от застревания.** Правило про остаток принадлежит управлению
задачами: оно решает, **сделана задача или вышла**, а это исход планирования, не
исполнения. **Ниже канонический текст; пайплайн проекта на него ссылается, а не
пересказывает** — два экземпляра одного правила разъезжаются, и разъезжаются
незаметно, потому что расхождение видно только на редком входе.
> Есть остаток, который доводится без ответа, — задача продолжается, вопрос
> записывается в файл. Остатка нет — задача выходит из спринта.
С двумя оговорками, без которых тест ошибается:
> **Остаток, который материализует нерешённое** — записывает в хранилище,
> журнал, витрину **или наружу** состояние, зависящее от неотвеченного вопроса,
> — **не остаток**. Решение поднимается до начала записи: откатить запись
> дороже, чем подождать ответ, а иногда невозможно. «Наружу» — часть правила, а
> не пример: выкладка, публикация и отправка данных третьей стороне не
> откатываются тем более.
> **Пол для остатка:** остаток, из которого пропала польза, названная в «зачем», —
> это не сделанная задача, а вышедшая из спринта.
## Заморозка набора
**Цель одна.** Набор служит ей; задача, не служащая цели, в спринт не попадает,
даже если взять удобно (`sprint take` это и запрещает). **Задача с открытым
вопросом в набор не берётся.**
**Новая работа падает в беклог, а не в идущий спринт.** Решение «врываться или
отложить» принимается один раз правилом, а не заново каждый раз. Врывается
только два класса:
1. **Необратимый ущерб** — потеря, порча или утечка данных: то, что не чинится
доделкой потом.
2. **Сломан общий станок** — красная проверка, на которой стоит определение
готовности **всех** задач набора. Это не новая работа, а починка того, на чём
делается вся остальная.
Что в проекте считается необратимым ущербом и что — общим станком, называет
`CLAUDE.md` проекта. Не названо — спрашиваем человека, а не решаем сами.
**Конец спринта** — когда каждая задача набора либо сделана, либо вышла с
записанной причиной. Не «все сделаны»: иначе одна застрявшая задача держит
спринт бесконечно. Пустой набор закрывается `sprint close` — скрипт не даст
закрыть непустой.
Ведение спринта целиком — исходы задачи, определение готовности, приёмка,
доклад — [references/sprint.md](references/sprint.md).
## Сессия: четыре шага в этом порядке
Это зависимость, а не список.
1. **Разбор вопросов.**
2. **Разбор прошедшего спринта — про процесс, а не про задачи.**
3. **Переоценка задач** порциями.
4. **Выбор цели и набор спринта.** Цель называет человек, набор собирает агент и
показывает **до старта работ**.
Рёбра подписаны тем, что ломается при их нарушении:
```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), и при расхождении прав текст.
Процедура каждого шага, размер и отбор порции, храповик на залежавшихся, формат
интерактива и доклад — [references/cadence.md](references/cadence.md).
## Инструмент
Тот же `tasks.py`, что у скилла `tasks` — оба скилла в одном плагине, путь
общий: `tk="$CLAUDE_PLUGIN_ROOT/skills/tasks/scripts/tasks.py"`. Сессии нужны
прежде всего:
```
python3 $tk check --dir D # с этого начинается любая сессия
python3 $tk list --dir D --questions # шаг 1: что накопилось
python3 $tk list --dir D --tag sprint:<слаг> # шаг 3: урожай спринта, первая порция
python3 $tk list --dir D --stale # шаг 3: дальше по залежалости
python3 $tk list --dir D --goal <слаг> # шаг 4: кандидаты под названную цель
python3 $tk sprint start --dir D --goal <слаг> # шаг 4: заводит и слаг спринта
python3 $tk sprint take --dir D <слаг> … # шаг 4: набор
python3 $tk sprint close --dir D # конец спринта; --dissolve при блокере
python3 $tk reopen <слаг> --dir D --reason … # приёмка не сошлась после закрытия
```
`D` — каталог задач проекта, по канону всегда `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` — его инструмент.
- **Занизить урожай** — не заводить найденное по ходу. Защита: поимённая сверка
с **сохранённым независимым отчётом**, а не с прозой исполнителя. Каждая
отложенная находка имеет либо слаг, либо строку «не заведена: причина».
Нулевой урожай при непустом отчёте виден сразу.
**Проект без конвейера ревью — независимого отчёта нет, и это надо сказать, а не
обойти молча.** Задачи делались руками или чужим пайплайном, сверять урожай не с
чем: остаётся проза исполнителя, то есть тот же взгляд, что и у автора. Тогда
защита от занижения урожая **снята**, и доклад спринта обязан нести строку «урожай
сверялся с отчётом исполнителя — независимого отчёта в проекте нет». Дальше это
решение человека: завести конвейер, принимать выборочной перепроверкой или
согласиться с ценой. Молчание здесь хуже любого из трёх исходов.
Стимулы внутри пайплайна задачи (занизить требования к проверке, пропустить
проход) принадлежат ему и защищены там же.
## Слоты проекта
Сессия не знает ни языка, ни сборки, ни CI. Часть проектного отвечает
[канон](../canon/references/canon.md) структурой: разбор процесса (шаг 2) живёт
в `docs/review.md`, оракулы и «чем краснеет безусловно» — в семантике гейта в
`CLAUDE.md`. Остальное проект **дописывает в `CLAUDE.md`**:
1. **Пайплайн задачи** — чем задача выполняется и что входит в его определение
готовности. Сессия требует только форму: пайплайн пройден + критерии приёмки
проверены поимённо.
2. **Общий станок** — какая проверка, покраснев, врывается в замороженный
спринт.
3. **Необратимое** — что спрашивается у человека всегда (тот же слот, что у
скилла `tasks`; дом один).
4. **Ориентир по размеру спринта**, если он замерялся. Умолчание — 5–8 задач, и
это **ориентир, а не закон**.
Слота «куда копируются критерии приёмки» здесь нет намеренно: на него отвечает
**пайплайн проекта**, перенося их в описание изменения при его заведении. Проект
без пайплайна называет своё место сам, в слоте 1.
Числа проекта (сколько задач в спринте, сколько времени на задачу, каков прирост
беклога) — предмет шага 2, а не константы этого скилла.
## Чего этот скилл не делает
Не пишет код и не выполняет задачи. Не заводит и не переоформляет задачи сам по
себе — формат и содержимое ведёт `tasks` (сессия зовёт его операции). Не решает
за человека, какая цель следующая. Не двигает набор идущего спринта.
@@ -1,221 +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. Разбор прошедшего спринта — про процесс, а не про задачи
Не «что мы сделали» (это доклад спринта, он уже был), а:
- **что сломалось в процессе и почему не поймали** — промах, доехавший до конца;
- **сколько на самом деле заняли задачи** против ожидания;
- **какие правила не сработали или сработали не так** — в том числе правила
этого плагина;
- **какие числа пора пересмотреть** — ориентир по размеру спринта, прирост
беклога на одну закрытую задачу, время на задачу. Эта обязанность иначе висит
ничья: числа, помеченные как «первый замер», не пересматриваются никогда, если
их не пересматривает конкретный шаг.
**Артефакт обязателен.** Вывод, оставшийся в контексте сессии, не существует:
следующая сессия его не увидит. Дом у него один и известен из канона —
**`docs/review.md`**: вывод про конвейер и про то, что перестали проверять, идёт
в раздел настройки, вывод про воспроизведённый дефект — в журнал. Решение с
долгим следом — в `docs/adr/`.
Отдельным ритуалом ретроспектива не выделяется: процесс личный,
синхронизировать некого.
## Шаг 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`. **Переоценка — то самое место, где беклог добирает род
работы и границы:** требовать их на входе значило бы выгонять в заметки то,
что должно лежать задачей, а к взятию в спринт они уже обязательны.
Затем — то, что решает пользователь:
6. **Жива ли она вообще.** Контекст мог измениться: ушла зависимость, отпал
сценарий, обошли иначе. Здесь и звучит вопрос о выкидывании.
7. **Та ли цель — и нужна ли она вообще.** Приоритетов нет, и «повысить» нечего
— вместо повышения **смена цели** (`edit <slug> --goal <другой>`) или
включение в ближайший набор. `feature`, которой не находится цель, — кандидат
на выход: новая возможность вне цели это возможность, которой никто не
заказывал. Операционной задаче (`fix`, `chore`, `research`) цель не нужна, и
выдумывать её здесь не надо.
8. **Задача ли это по-прежнему.** Не проходит тест «готова к взятию» → `edit
<slug> --type idea`, дальше штурм. Разрослась → это несколько задач под той
же целью, дальше декомпозиция.
9. **Переоценка по измеренному.** Спринт производит числа — сколько на самом
деле стоит такая работа, что оказалось дороже ожидания. Эти числа меняют цену
**других** задач, и именно здесь это применяется: задача, чья цена выросла
втрое, а польза осталась прежней, — кандидат на выход.
### Храповик на залежавшихся
Сильно залежавшаяся задача — сигнал сама по себе: её либо ни разу не собирались
делать, либо нечем взять. Измеряй наблюдаемым — датой последней правки из git
(`list --stale` ставит такие первыми); счётчик «сколько сессий пережила» нигде
не хранится.
Задача из верхних строк `--stale`, которую и этот заход оставляет без изменений,
**либо двигается (меняет цель, идёт в набор, уходит с причиной), либо остаётся с
явно записанной причиной**, почему её держим (`move <slug> --section <та же>
--reason …`). Молчаливое «оставить как есть» на давно неподвижной задаче — это
решение не принимать решение; запись причины превращает его в осознанное и не
даёт тому же вопросу всплыть на следующей сессии.
### Интерактив
- Вопросы — через `AskUserQuestion`, **не больше трёх за раз**. Порция в 5–8
задач обычно даёт больше трёх суждений — тогда веди несколько итераций по ≤3,
а не по одному на задачу и не одним перегруженным запросом.
- К каждому варианту — **предварительное суждение, рекомендация первым
вариантом**: «предлагаю выкинуть, потому что …». Пользователю дешевле
возразить, чем судить с нуля.
- Всё, что решается фактом (сделано / отменено / дублируется), решай сам и
показывай списком в докладе, а не выноси в вопросы.
Пример одной итерации — три залежавшихся задачи, механику по ним уже разобрали:
> **Переоценка: 3 залежавшихся (порция по `--stale`)**
>
> 1. `versii-kachestvo-repaki` — версии и качество одного тайтла
> - Выкинуть *(рекомендую)* — помечена «не боль», за полгода ни разу не возникла
> - Оставить под целью `nadyozhnost-razdach`
> - Перевести под цель `kachestvo-mediateki` — там она первая в очереди
> 2. `backup-sqlite` — бэкап базы
> - Оставить под текущей целью *(рекомендую)* — не сработала, но риск реальный
> - Взять в ближайший набор — без бэкапа ретеншн опасен
> - Выкинуть
> 3. `guessit-sputnik` — вынести распознавание в сервис-спутник
> - Понизить до `[idea]` *(рекомендую)* — не проходит тест «готова к взятию»
> - Оставить задачей
Каждый вариант несёт причину — ту самую, что уедет в `--reason`. Ответы применяй
сразу и, если в порции осталось ещё, следующей итерацией показывай следующие ≤3.
## Шаг 4. Выбор цели и набор спринта
1. **Покажи состояние проекта**: секцию `Готово` (что приложение уже умеет —
это половина ответа на «где мы»), затем `Запланировано` с обоснованием
очереди, `Направления`, и
по каждой цели-кандидату — сколько под ней задач без открытых вопросов
(`list --goal <слаг>`). Цель без готовых задач набором не станет: её сперва
надо декомпозировать.
2. **Цель называет человек.** Это продуктовое решение, а не механика: агент
предлагает и объясняет, но не выбирает.
3. **Набор собирает агент** — `sprint start --goal <слаг>`, затем `sprint take
`. Скрипт не даст взять цель, идею, задачу с чужой целью, с открытым
вопросом, без критериев приёмки, без рода работы или без раздела
«Затрагивает». Задача без цели вовсе (`fix`, `chore`, `research`) берётся
свободно — операционная работа входит в набор помимо его цели.
4. **Набор показывается человеку до старта работ.** Показ — это и есть момент
заморозки: после него набор не двигается. **В показе называется состав по
роду работы** — три `fix` и ни одной `feature` под целью развития это
разговор про цель, а не про набор, и увидеть его надо до заморозки, а не в
докладе по итогам.
Здесь же последний дешёвый момент заметить **разнородную задачу**: раздел
«Затрагивает» показывает границы до того, как заведено предложение об
изменении. Строка, которая одна тянет задачу на ступень выше остального
перечня, — кандидат на разрез (шов — в `tasks`, `references/split.md`).
Замеченная здесь, она стоит одного `edit`; замеченная на ревью — выброшенного
предложения.
5. Задача, которой для взятия не хватает только критериев приёмки, границ или
рода, дописывается здесь же — 2–5 утверждений с оракулами, перечень
затрагиваемых границ, `--kind`. Но если для этого нужен ответ человека, это
вопрос, и задача в набор не идёт.
**Размер — ориентир, а не закон:** 5–8 задач. Можно взять больше, можно меньше —
набор под цель важнее круглого числа; одна крупная задача спринтом тоже бывает.
## Доклад сессии
- Что просмотрено: N из M, сколько порций, по какому признаку отобраны.
- Вопросы: разобрано N, из них отвечено без человека N, снято тегов N.
- Разбор процесса: что записано и куда.
- Изменения списком: удалено как реализованное (со ссылками), ушло без
реализации (с причинами), понижено до идей, слито, сменило цель.
- Новый спринт: цель, набор со слагами, дата, состав по роду работы.
- **Границы покрытия**: сколько задач не трогали и какие именно секции, теги или
цели остались — иначе доклад читается как «беклог разобран».
- `tasks.py check` после правок — результат строкой.
@@ -1,186 +0,0 @@
# Ведение спринта
Спринт — набор задач под одну цель, замороженный до его конца. Здесь то, что
происходит **внутри** спринта: как задача заканчивается, что считается сделанным,
кто принимает и что идёт в доклад. Как спринт набирается — шаг 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 …`), недоделанное возвращается в беклог, новый набор
делается после ответа человека. Спринт не «ждёт»: ждать может человек, а
замороженный набор, который нельзя двигать, только мешает.
## Определение готовности
Задача засчитывается сделанной, когда верно **всё**:
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⌉` порций.
- **Границы покрытия** сжатой строкой: что в этом спринте не проверялось вовсе.
-619
View File
@@ -1,619 +0,0 @@
---
name: tasks
description: Ведение задач и целей как каталога markdown-файлов (одна запись = один файл в items/ + строка в одном из индексов). Заведение задачи, идеи или цели из диалога, разбор находок аудита/ревью, декомпозиция на независимо полезные части, мозговой штурм идеи, гигиена полей и проверка согласованности индексов. Использовать, когда просят добавить задачу/идею/цель, превратить находки ревью в задачи, разбить задачу, проработать идею, поправить формат или проверить беклог. Ритуал между спринтами — скилл session. Не реализует задачи — этим занимается пайплайн проекта.
---
# Задачи
Задачи — каталог markdown-файлов. Одна запись = один файл `items/<slug>.md` плюс
строка **ровно в одном** индексе. Скилл владеет **форматом и содержимым**:
заводит, редактирует, закрывает, разбирает находки ревью, дробит, штурмует идеи.
Чем он **не** владеет: ритуалом между спринтами (разбор вопросов → разбор
прошедшего спринта → переоценка → выбор цели и набор) — это скилл `session`; и
выполнением задачи — это пайплайн проекта.
## Пять правил, из которых всё следует
Ситуация не покрыта инструкцией — решай по ним.
0. **Цель — возможность приложения, задача — шаг к ней.** Цель отвечает на «что
приложение будет уметь», её «Завершение» — наблюдаемый признак того, что уже
умеет; задача отвечает на «что для этого нужно сделать». Оценивается проект
по **поведению**, а не по внутреннему устройству, поэтому и роадмап отвечает
не «сколько работ осталось», а «что уже умеет и чего ещё не умеет».
Свойство поведения — тоже возможность: «сообщает о своём состоянии»,
«исход слияния не зависит от порядка доставки» — законные цели.
1. **Беклог гниёт с той стороны, где его пополняют.** Заведение — самая частая
операция и с худшим отказом: из одного разговора рождается пять файлов, а
переоценка потом разгребает то, чего не надо было заводить. Дедупликация и
фильтр на входе дешевле любой чистки. Заводим только то, что **не делаем
сейчас** и о потере чего пожалеем.
2. **Файл — источник истины, индексы производны.** Разошлись — неправы индексы.
Согласованность механизируема и проверяется командой, а не вниманием: всё,
что ловит `tasks.py check`, не должно попадать ни в чек-лист, ни в промпт.
Поэтому **«зачем» живёт в мете файла**, а строка индекса его лишь
повторяет: пока поле лежало только в индексе, восстановление пропавшей
строки теряло его молча и навсегда. Единственное исключение намеренное: **в каком
индексе лежит задача, знают индексы** — «в спринте» это свойство спринта, а
не файла, поля-состояния нет.
3. **Причина переживает запись.** Выкинутая без причины задача вернётся через
квартал тем же текстом. Реализованная оставляет след в коммите — выкинутая не
оставляет ничего, поэтому у неё есть `REJECTED.md`.
4. **Порядка нет, есть цель — но цель есть не у всякой задачи.** Приоритетов,
«повысить» и «встать раньше» нет: «что делать дальше» отвечает набор спринта,
а между спринтами порядок не нужен никому. Цель обязательна там, где она и
есть содержание работы, — у **новой возможности** (`kind:feature`). Починка,
техдолг и разведка служат работоспособности, а не направлению, и живут без
цели законно; в набор спринта они входят помимо его цели. Придуманная им цель
— то же враньё, от которого спасает род работы.
## Раскладка
Каталог задач — **`docs/tasks`, жёстко**: это часть
[канона документов](../canon/references/canon.md), и подгоняется под него
проект, а не наоборот.
```
docs/tasks/
items/ задачи и цели файлами, <slug>.md, слаги английские
ROADMAP.md состояние проекта: что уже умеет и чего ещё не умеет
BACKLOG.md что можно взять — только задачи, целей здесь нет
SPRINT.md текущий спринт: цель, набор, дата
REJECTED.md ушедшее БЕЗ реализации, с причиной и датой
```
Правило, снимающее путаницу: **`BACKLOG.md` — то, что берут; `ROADMAP.md` — то,
подо что берут.** Цель в спринт взять нельзя, поэтому в списке берущихся ей не
место.
**Четыре секции роадмапа, и первая отвечает на половину вопроса:**
| Секция | Англ. | Что в ней |
| --- | --- | --- |
| `Готово` | `Done` | достигнутые цели — строкой с датой, **без ссылки на файл**: файл удалён, поведение живёт в спеках |
| `Запланировано` | `Planned` | очередь значима и обосновывается прозой рядом |
| `Направления` | `Directions` | очереди нет, тянутся долго |
| `Разработка` | `Tooling` | инструмент и процесс — не возможности приложения, и потому отдельно |
**Секции роадмапа канонические, секции беклога — нет**, и разница не в любви к
единообразию. У каждой секции роадмапа свой смысл, в первую пишет сам `close`, и
роадмап, названный по-своему, читался бы только своим автором. Секции беклога
(`Ядро`, `Инфра`) смысла не несут — это полки, и остаются делом проекта.
Отсюда три правила, которые проверяет `tasks.py check`: **состав закреплён**
(чужая секция — ошибка, а не вольность), **все четыре обязаны быть** (нет
секции — нет ответа на её часть вопроса), **язык один на весь индекс**.
`--roadmap-sections` у `init` нет: выбирать нечего.
**Заголовок секции — с прописной, после него пустая строка.** Во всех индексах
одинаково, включая секции беклога, которые проект называет сам. Написание
канонических секций правит `check --fix` (заодно и ссылку на секцию в мете
файлов: имя секции принадлежит заголовку индекса, файл на неё только
ссылается); отбивку он ставит везде.
Оговорка про `Разработка`: слово `окружение` сюда не годится — в
`architecture.md` оно уже значит боевое окружение приложения, и одно слово в двух
смыслах развело бы документы канона.
**Секции «блокеры» в беклоге нет.** Блокер — это *состояние* (спринт не может
продолжаться ни одной задачей), а не полка: он живёт ровно до ответа человека, и
записи в такой секции не успевают жить. Следы блокера остаются вопросами в
файлах задач распущенного спринта. Постоянно пустая секция со старой семантикой
«разбираются пачками» противоречила бы правилу «блокер эскалируется немедленно»,
поэтому `init` её заводить отказывается, а `check` о ней говорит. **Проекту,
который переезжает с такой секцией, её надо удалить** — это единственное место,
где это сказано.
**Задача живёт в одном индексе за раз.** Взята в спринт — строка переезжает из
`BACKLOG.md` в `SPRINT.md`; вышла — обратно. Файл в `items/` при этом **не
двигается**: он и есть запись, индексы лишь показывают, где она числится.
**У сделанной задачи записи не остаётся** — файл и строка удаляются (`close
--implemented`). Ей хватает коммита и документации проекта; вторая запись была
бы вторым домом для того же факта. Вопрос «что было в спринте N» отвечается
даром: `SPRINT.md` лежит под git, `git log -p docs/tasks/SPRINT.md` отдаёт историю
всех наборов без отдельного журнала.
**У достигнутой цели запись остаётся, и это единственное исключение.** Файл
удаляется так же, а строка переезжает в секцию `Готово` с датой. Причина в том,
что цель — не работа, а **возможность**: «что приложение умеет» это половина
вопроса, ради которого роадмап и открывают, и стирать её вместе с файлом значит
оставить инструмент, отвечающий только «что осталось». Вторым домом это не
становится: поведение живёт в `openspec/specs/`, а роадмап отвечает **когда и в
каком порядке оно появилось** — другой вопрос. Ссылки на файл в строке нет
намеренно: файл удалён, а битая ссылка — законная ошибка `check`.
Куда запись может переехать и какой командой — весь набор переходов:
```mermaid
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
[*] --> B: add
[*] --> P: add --type goal
B --> P: edit --type goal --section
P --> B: edit --type task --section
B --> S: sprint take
S --> B: sprint drop --reason
S --> 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
A --> P: reopen --reason
```
Состояния здесь — **где числится строка**, а не где лежит файл: файл
`items/<slug>.md` не двигается ни на одном переходе. Стрелок «руками» на схеме
нет намеренно — каждый переход это команда, и другого способа его совершить не
существует.
Схема — **сводка**: условия и оговорки живут в тексте разделов, и при
расхождении прав текст.
## Цели
**Цель — возможность приложения.** Такой же файл в `items/`, тип `[goal]`,
перечисленный в `ROADMAP.md`. Формулируется ответом на вопрос **«что приложение
будет уметь»**, а не названием области работ: не «Работа с чтением», а «Чтение
данных клиентами»; не «Рефакторинг слияния», а «Исход слияния не зависит от
порядка доставки».
**Свойство поведения — тоже возможность.** «Наблюдаемость» это «приложение
сообщает о своём состоянии»; «прочность слияния» это «исход не зависит от
порядка». Такие цели законны и переформулировки в функцию не требуют — требуют
только, чтобы формулировка отвечала на «что приложение делает», а не на «какую
часть кода мы трогаем».
**Что целью не является — работа над инструментом и процессом.** Сборка,
проверки, сам этот скилл: на вопрос «что приложение будет уметь» они не
отвечают. Им
отведена отдельная секция роадмапа, чтобы они были видны в том же экране и при
этом не читались как возможности продукта.
Секция выбирается так: очередь значима и обоснована прозой — `Запланировано`;
тянется долго и очереди не имеет — `Направления`; не про приложение —
`Разработка`; в `Готово` кладёт сам `close`.
- **Список задач цели выводится, а не хранится.** В теле цели — зачем она и что
считается её завершением; перечня задач там нет. Он был бы третьим индексом и
поехал бы на первой же закрытой задаче, а `check` про него не знает. Связь
однонаправленна: задача несёт тег `goal:<слаг>`, перечень даёт
`tasks.py list --goal <слаг>`.
- **Статус цели выводится.** Цель достигнута, когда у неё не осталось открытых
задач; `[x]`/`[~]` руками не ведутся, а `close` цели с живыми задачами
скрипт запретит. Достижение — `close <цель> --implemented`: файл удаляется,
строка с датой переезжает в `Готово`. Ошиблись — `reopen` вернёт файл и
**снимет строку достигнутого**, чтобы роадмап не утверждал того, чего нет. Единственная оговорка: цель без задач неотличима — «ещё не
разобрана» или «всё закрыто». Различает **тег `decomposed`** в мете
цели: он ставится, когда цель разложена на задачи. Тег, а не строка в теле —
потому что проверяется механически: `check` **напоминает** о нём у пустой цели
(замечанием, не ошибкой — неразобранная цель это законное состояние), а `check
--fix` сам проставляет его цели, у которой задачи есть.
- **Тип `[epic]` упразднён.** Он был зонтиком между целью и задачами — «задача,
которая не мерджится целиком». Зонтик теперь цель, а слишком крупный шаг просто
дробится на шаги помельче под той же целью, и промежуточному типу места не
осталось. Замер подтвердил: ноль употреблений на 97 записей двух живых
проектов. Встретился в чужом беклоге — это цель либо набор задач, и `check`
назовёт его неизвестным типом.
## Род работы
**Тип записи и род работы — две оси, и путать их нельзя.** Тип отвечает «что это
за запись» (цель, идея, задача), род — «какого рода работа»: `feature`, `fix`,
`chore`, `research`. Одним значением на оба вопроса не ответить: идея бывает
*про* функцию, а цель функцией *и является*.
- **`feature`** — снаружи появляется или меняется то, чего раньше не было.
- **`fix`** — поведение расходится с заявленным, и расхождение воспроизводится.
Не воспроизводится — это `research`, а не `fix`.
- **`chore`** — обслуживание: зависимости, сборка, перенос, чистка. Наблюдаемое
поведение не меняется, и в этом всё дело: **у `chore` тест готовности слабее
честно**, а не молча. «Что станет наблюдаемо иначе» здесь отвечается
разработчику («перестанет собираться два раза», «уедет последний вызов
устаревшего API»), а не пользователю. Пока рода не было, такие задачи либо не
заводились, либо формулировались как выдуманная польза.
- **`research`** — исход работы знание, а не изменение системы: ответ на вопрос,
замер, разведка. Приёмка — записанный ответ (`docs/research/`, ADR, тело
задачи), а не изменённый код.
Дом рода — **тег `kind:<род>`**, а не префикс заголовка и не поле меты: теги
здесь единственный механизм разметки, и `list --kind fix` работает даром. Цена
известна: в строку индекса род не попадает (индексы производны), и «в наборе одни
починки» видно командой, а не глазами по `SPRINT.md`.
Словарь **закрыт**. Открытый разъедется на синонимах — `bug`, `bugfix`, `fix`,
`defect`, — и отбор по роду перестанет отвечать на свой единственный вопрос. Ни
один род не подходит — это сигнал, что в задаче их два и её надо разделить.
**Род обязателен у задачи, у цели запрещён, у идеи необязателен** — идея получает
его, когда становится задачей. Требуется он там, где по нему принимают решение:
`sprint take` без рода откажет. `check` о пропаже только **напоминает** — беклог,
заведённый до появления рода, законен, и переоформлять его «заодно» здесь не
просят.
**Род решает и то, обязательна ли цель.** `feature` без цели не бывает: новая
возможность и есть содержание цели, и если подходящей нет — либо она заводится,
либо это не `feature`. `fix`, `chore` и `research` живут без цели законно, и
`check` о них молчит: они служат работоспособности, а не направлению. Это
единственный случай, когда род что-то определяет за пределами отбора, — и
определяет он учёт, а не процесс проверки.
**Род не выбирает профиль ревью и вообще ничего не предписывает пайплайну.**
Профиль выбирается по факту изменения, а не по роду задачи: `chore` бывает
миграцией схемы, `fix` — правкой публичного контракта. Правило «предписание
процесса в теле задачи снимается» родом не отменяется, а подтверждается: он
описывает работу, а не то, как её проверять.
## Как написана задача
Три требования к тексту. Первое — про заголовок, два остальных про то, чтобы
задачу можно было **оценить, не открывая код**.
**Заголовок отвечает на вопрос своего типа.** Вопросов три, поэтому и форм три:
| Тип | Отвечает на | Пример |
| --- | --- | --- |
| цель | что приложение будет уметь | Соперником может быть компьютер |
| задача | что нужно сделать | Печатать поле одним куском кода |
| идея | о чём она | Подсказка следующего хода |
Задача — **глаголом в неопределённой форме**, перед ним допускается «не»: «Не
отбрасывать молча лишние символы в ходе», а не «Лишние символы молча
отбрасываются». Описательный заголовок называет **состояние**, а из состояния не
видно, чего от работы ждут: «Ничья объявляется, пока клетки есть» одинаково
читается и как жалоба, и как задание, — и в списке, где решают «брать или не
брать», это разные вещи. Идея формы действия не несёт **намеренно**: что делать,
ещё неизвестно, и заголовок-действие обещал бы решённость, которой нет.
Из этого же правила растёт разница индексов: роадмап — список возможностей,
беклог — список работ, и если заголовки перепутать формами, каждый из них
начинает читаться как другой.
`check` считает заголовки не в форме действия и печатает **число** в блоке
здоровья, не замечанием на файл: проверка эвристическая (первое слово на
`-ть`/`-ти`/`-чь`), а беклог, заведённый до правила, не переоформляют «заодно».
Годность формулировки — не машине: её смотрит
[агент вычитки](#вычитка-формулировок).
**Функции и границы, а не намерения.** Задача называет, что система начнёт
делать, и какие границы это трогает: эндпоинт или команду, таблицу и миграцию,
формат на диске, публичный тип пакета, внешний сервис. Перечень живёт разделом
«Затрагивает» (форма — [references/task-format.md](references/task-format.md)) и
требуется к взятию в спринт. Без него задача оценивается по объёму текста, а не
по объёму поверхности, — и оценка систематически занижена ровно там, где текст
короткий, а границ много. Названы **границы**, а не то, как они изменятся: план
реализации живёт в предложении об изменении, а не в задаче.
**Предметно, но без усложнения.** Текст задачи читает человек, который решает,
брать её или нет, и делает это по строке индекса и одному экрану тела.
Язык — общий для всех проектных текстов, и живёт он одним файлом:
[../canon/references/language.md](../canon/references/language.md)
(информационный стиль, применённый к задачам и документам канона; там же таблицы
англицизмов и жаргона и то, что из стиля отброшено намеренно). Задаче он даёт
четыре требования, которые нарушаются чаще прочих:
- **глагол вместо отглагольного существительного**: «обработчик не проверяет
владельца», а не «проверка владельца не осуществляется»;
- **факт вместо оценки**: «время ответа доходит до 800 мс», а не «работает
медленно». Оценка без факта рядом — настроение, а не сведение;
- **англицизм с живым русским аналогом заменяется**: не «зафиксить флоу», а
«починить порядок доставки». Имя вещи не переводится: слаг, команда, тип в
коде, `API`;
- **термин не из документов проекта вводится одной строкой** или не
употребляется. Свой словарь у задачи — самый дешёвый способ сделать беклог
нечитаемым для того, кто вернётся к нему через квартал.
И одно требование, которое есть только у задачи: **сложность формулировки — не
признак сложности работы.** Задачу, которую не удаётся сказать просто, чаще
всего не удаётся и оценить: это либо две задачи, либо идея.
Эти правила — про **язык**, а не про объём: короткая задача без границ хуже
длинной с ними.
## Инструмент (`tasks.py`)
Пусть `tk="$CLAUDE_PLUGIN_ROOT/skills/tasks/scripts/tasks.py"`, а `D`
`docs/tasks` от корня проекта. `--dir` стоит в примерах намеренно: вызов из
подкаталога — обычное дело.
```
python3 $tk check --dir D # согласованность индексов + здоровье
python3 $tk check --dir D --fix # + починить дрейф (секция, заголовок, дубли, «зачем», форма меты)
python3 $tk list --dir D [--stale] [--section S] [--type T] [--kind K] [--tag a,b] [--goal S] [--index …] [--questions]
python3 $tk add --dir D --slug S --title T [--type goal|idea] [--section S] [--goal G] [--kind K] [--why «зачем»] [--tag a,b]
python3 $tk edit S --dir D [--title T] [--why «зачем»] [--type T] [--goal G] [--kind K] [--add-tag a,b] [--rm-tag c]
python3 $tk move S --dir D --section S [--reason R] [--after S | --first]
python3 $tk close S --dir D --reason R # в REJECTED.md + удалить (ушла без реализации)
python3 $tk close S --dir D --implemented # просто удалить (реализована и закоммичена)
python3 $tk reopen S --dir D --reason R # вернуть закрытую: приёмка не сошлась
python3 $tk sprint start --goal S --dir D | take S… | drop S… --reason R | close [--dissolve --reason R]
python3 $tk init --dir D [--sections …] [--roadmap-sections …] [--items …] [--backlog …] …
python3 $tk adopt scan --from … | apply --plan … # разовая адаптация, references/adopt.md
```
**Коды выхода — единый словарь; на нём ветвятся скиллы, а не на тексте вывода:**
| Код | Что случилось | Что делать |
| --- | --- | --- |
| 0 | сошлось / сделано | дальше по сценарию |
| 1 | **только `check`:** найден дрейф индексов и файлов | `check --fix`, остаток разобрать |
| 2 | ошибка употребления: аргументы или нарушенное правило | читать сообщение, это отказ по существу |
| 3 | окружение: каталог не найден, конфиг битый или мимо диска | чинится путём или `docs/.pm.json`, повтор не поможет |
| 4 | внутренний сбой | дефект скрипта, доложить |
Различать 1 и 3 обязательно: «дрейф в беклоге» — рабочая ситуация, «каталога
нет» — нерабочая, и одинаковая реакция на них была бы неверна в обоих случаях.
Тип — английское ключевое слово `goal` / `idea` / `task` (как и прочие токены
команд); `task` префикса не несёт, остальные кодируются `[goal]`/`[idea]` в
заголовке. Текст задачи при этом русский.
**Мутации правят файл и индексы заодно** — руками строку индекса или мету
не пиши, зови `add`/`edit`/`move`/`close`/`sprint`. Смена заголовка, «зачем», типа,
цели, рода работы и **тегов** — это `edit`: он держит H1, мету и индекс в синхроне.
Снятие тега — `--rm-tag` (после ответа на вопрос снимается `question`), смена
цели — `--goal`, рода — `--kind`; оба заменяют прежнее значение, а не добавляют
второе.
**Переезд между индексами — следствие смены типа, а не отдельная команда.**
`edit <slug> --type goal --section <часть роадмапа>` переносит строку из
`BACKLOG.md` в `ROADMAP.md` (и обратно `--type task --section <секция беклога>`);
`move` двигает только внутри одного индекса и пишет причину. `--section` у
`edit` работает **только** при таком переезде — иначе он отсылает к `move`,
потому что смена секции без причины и есть тот дрейф, который потом никто не
объяснит. Задача в наборе спринта тип не меняет вовсе: сперва `sprint drop`.
Тело задачи скрипт не трогает:
`add` кладёт заголовок, мета-блок и шаблон с подсказками, тело дописываешь
редактором (пока плейсхолдер на месте, `check` напоминает).
`check` — единственный судья согласованности; что именно он ловит, скажет его
вывод, здесь не пересказываем. Гоняй его **в начале сессии** и **после каждой
правки**, даже если правил мутациями: дрейф мог накопиться раньше. Накопившееся
чини `check --fix` — он детерминированно правит то, где истина однозначна
(секция, заголовок, дубли, «зачем» из индекса в файл, старая форма меты,
пометка `decomposed` у цели с задачами), а неоднозначное (задача сразу в двух
индексах, нечего восстанавливать) печатает отдельной пометкой `НЕОДНОЗНАЧНО`
это тебе, и это идёт строкой доклада. **Ссылка на исчезнувший файл в пометку не
попадает:** `--fix` её просто не трогает, и она остаётся `ОШИБКА` обычного
`check` — то есть видна, но в докладе её надо назвать отдельно.
`--fix` правит **и файлы** — ровно в двух местах, где источник ровно один и
выбирать не из чего: «зачем», оставшееся только в индексе, переезжает в мету,
и цель, у которой есть задачи, получает тег `decomposed`. Оба случая печатаются
поимённо.
**Что механизировано, а что нет.** У задачи, взятой в набор (`sprint take` и
`check` по задачам спринта), проверяются три вещи, и у каждой своя глубина:
- **критерии приёмки** — число пунктов жёстко (меньше двух отказ, больше пяти
замечание), наличие оракула **эвристикой** по слову «оракул» в пункте;
- **род работы** — жёстко: назван и из закрытого словаря;
- **раздел «Затрагивает»** — только **наличие непустого**. Полнота перечня машине
не видна: границу, которую забыли назвать, она от отсутствующей не отличает.
Настоящий оракул от слова «оракул» машина тоже не отличает, поэтому эвристика
даёт только замечание, и в докладе это называется как есть: «проверено число
пунктов и наличие границ, годность оракулов и полнота границ — глазами».
Формат файла, меты, слага, индексов и `REJECTED.md`
[references/task-format.md](references/task-format.md). Там же тест «готова к
взятию», требования к критериям приёмки и раздел «Затрагивает».
## Сценарии
### Завести задачу, идею или цель из диалога
1. **Фильтр.** Делаем прямо сейчас — не заводим. Не пожалеем о потере — не
заводим. Родилось три кандидата — покажи их и спроси, какие заводить: молча
заведённая пачка и есть тот самый отказ из правила 1.
2. **Дедуп.** `list` плюс поиск по слагам, полю «зачем» и телам (`grep -ril`),
**включая `REJECTED.md`**. Нашлось среди живых — **дописываем в существующий
файл**, а не заводим соседний. Нашлось в `REJECTED.md` — покажи пользователю
ту строку и что изменилось с момента отказа (`add` предупредит и сам, но
молча заводить нельзя). Две задачи об одном — самая дорогая находка
переоценки.
3. **Тип по тесту готовности** (см. task-format): проходит — задача, не
проходит — идея (`--type idea`). Не делается одним заходом — это не эпик, а
несколько задач под одной целью: дроби сразу. Возможность приложения, а не
шаг — цель (`--type goal`).
4. **Цель задачи — если род её требует.** У `feature` должен быть
`--goal <слаг>`: новая возможность и есть содержание цели. Подходящей нет —
либо она заводится (`--type goal`), либо перед тобой не `feature`. У `fix`,
`chore` и `research` цели может не быть вовсе, и придумывать её не надо. У
идеи цель проставляется, когда идея становится задачей.
5. **Род работы**`--kind feature|fix|chore|research` (см. «Род работы»). Не
подходит ни один — задача не одна, разбирай.
6. `add …`, затем допиши тело редактором: одна фраза, **затрагиваемые границы**,
критерии приёмки с оракулами, рамки. «Зачем» отвечает «зачем нужна эта
задача» — состояние, остаток, боль, — а не пересказывает первый абзац, и
пишется **для человека**: не «канонизация внутри транзакции», а «тело 40 МиБ
держит блокировку 5 секунд, соседние доставки уходят в отказ».
7. `check`.
### Разобрать находки аудита или ревью
Ревью и аудиты — тоже источник задач, но с зеркальной диалогу опасностью: не
пять файлов из одной мысли, а сорок файлов из сорока сырых находок. Защита та
же, что в самом ревью: кластеризация по причине, дедуп против живых и
`REJECTED.md`, находка без свидетельства → идея, а не задача, и карта кластеров
пользователю до создания файлов. Порядок, отображение серьёзности и привязка к
целям — [references/from-review.md](references/from-review.md).
### Прийти в репозиторий, где задачи уже как-то ведутся
Разовая операция: вывести каталог задач из старой раскладки беклога, `TODO.md`,
заметок или списка шагов роадмапа — [references/adopt.md](references/adopt.md).
Сюда же относится переименование транслитных слагов в английские: оно делается
**одним проходом вместе с починкой перекрёстных ссылок**, а не по одному слагу.
Если переводить надо не только задачи, а весь `docs/` — это скилл
`av-dev-pm:canon`, и он зовёт этот сценарий сам на своём шаге.
### Декомпозиция и штурм идеи
[references/split.md](references/split.md). Обе операции превращают одну запись в
несколько, и у обеих есть проверяемый тест: части должны **мерджиться порознь** и
**каждая давать видимую пользу**, а у штурма исход «выкинуть» — полноправный.
Там же **шов**: где резать, когда допустимых мест несколько. Коротко — по
границе, которая одна поднимает ступень ревью выше остальных; и не резать, когда
обе половины остаются в одной ступени, потому что несокращаемый костяк проверок
платится за каждую задачу отдельно.
### Вычитка: два прохода, а не один
Записи судит **не тот агент, который их написал**: самопроверка текста слабее
всего ровно там, где формулировка казалась удачной при написании. Проходов два,
и они разные по природе:
| Проход | Что смотрит | Над чем работает |
| --- | --- | --- |
| `task-form` | заголовок по типу, «зачем» вместо пересказа, границы вместо замысла, годность оракулов, предписание процесса, связь со строкой «Завершения» цели | только `items/`, **открывает файл цели** |
| `doc-wording` | залог и отглагольные, оценка без факта, стоп-слова, англицизмы, жаргон, неизвестный термин | любой проектный текст, включая документы канона |
Разделены они не по охвату, а **по глубине**. Язык проверяется по словам и
фразам, поштучно; форма записи требует понять, что задача делает, и открыть
цель, на которую она ссылается. Слитый проход одну половину делает дорогой, а
вторую — поверхностной. Отсюда и разные модели.
Каждый устав отказывается от чужой половины прямо: увиденное не по своей части
идёт **строкой в границах покрытия**, а не находкой. Две проверки одного места
расходятся и начинают спорить, и разнимать их потом дороже, чем не сводить.
**Порядок — сперва `task-form`.** Его находки меняют решение «брать или не
брать», а язык — только цену чтения; и переписанный заголовок бессмысленно
вычитывать до того, как он переписан.
Зовутся они **пачкой, а не на каждую запись**: после заведения нескольких задач,
после разбора находок ревью и на переоценке. Передаётся список файлов и — если
есть — паспорт, архитектура и конвенции проекта: по ним отличается неизвестный
термин от известного.
Ни один из них ничего не правит. Оба возвращают готовые формулировки, и их
подставляет скилл: заголовок — `edit <слаг> --title …`, «зачем» —
`edit <слаг> --why …`, остальное редактором. **Заголовок и «зачем» — это то, по
чему задачу выбирают, поэтому менять их молча нельзя**: покажи предложенное
пользователю вместе с тем, что было. Правки в теле (границы, критерии, язык)
применяются сразу.
Всё, что ловит `tasks.py check`, оба не трогают намеренно.
### Гигиена полей
Правится по ходу любой операции, которая задачи касается (но не «заодно» по
всему беклогу):
- **протухшее «зачем»** — задача изменилась, а поле отвечает на старый вопрос;
особенно после ответа на вопрос задачи: «Решено: …» на «почему это лежит в
беклоге» уже не отвечает. Переписывается `edit <slug> --why …` — он правит
мету файла и строку индекса заодно;
- **вопрос, застрявший в прозе** — вынимается в раздел «Вопросы» плюс тег
`question` (`edit --add-tag question`), иначе он не виден ни `list
--questions`, ни правилу «задача с открытым вопросом в набор не берётся»;
- **тег, который некому снять** — `question` после ответа снимается `edit
--rm-tag question` вместе с записью ответа в тело **и опустошением раздела
«Вопросы»**: судит раздел, а не тег (`references/task-format.md`);
- **свойство репозитория в рамках** — номер миграции, хеш, версия зависимости:
в лежалой задаче протухает молча и становится ложной рамкой. Снимается;
снимок берётся при постановке, а не при заведении;
- **предписание процесса в теле** — «делать таким-то профилем ревью», «взять
такой-то агент»: это второй дом для правила выбора и путь понизить требования
решением, принятым до проектирования. Снимается;
- **род, разошедшийся с задачей** — задача заводилась починкой, а после разбора
оказалось, что поведение никогда и не было заявлено: это `feature`, а не `fix`.
Правится `edit <slug> --kind …`; род, оставшийся от прошлой формулировки, врёт
ровно там, где по нему отбирают;
- **границы, названные вместо реализации** — «переписать хранилище на новый
драйвер» в разделе «Затрагивает» это не граница, а замысел. Границы —
`таблица points и её миграция`, `эндпоинт POST /ingest`, `формат отпечатка на
диске`. Переписывается перечнем;
- **англицизм и термин из ниоткуда** — правится по ходу той же операции, что
касается задачи (см. «Как написана задача»). Именно по ходу: беклог не
переписывают ради языка.
## Переносимость
Скилл независим от **языка программирования, сборки, CI и трекера**: он ничего
не знает ни про Go, ни про npm, ни про конкретный багтрекер — задачи для него
просто каталог markdown. Текст задач — русский (язык документации проекта);
зашита только латиница слага. OpenSpec ему тоже не нужен.
- **Каталог задач — `docs/tasks`, жёстко**, и `--dir` передаётся явно всегда:
раскладка канона одинакова во всех проектах, и искать больше нечего. Каталога
нет — код 3 и вопрос человеку; `init` заводит его **только** когда проект
действительно новый, а перевод чужой раскладки делает `av-dev-pm:canon`.
У скрипта поиск вверх по дереву ещё жив — он для непереведённых проектов, и
полагаться на него скилл не должен: молча найденный чужой каталог это дрейф.
- **Настройки живут в `docs/.pm.json`**, ключ `tasks`: **имена** файлов и
заголовков, и только если они отличаются от умолчания. Один конфиг на весь
канон, а не по одному на каталог. Неизвестный ключ — код 3 на любой команде,
так что лишнее слово в этом объекте останавливает работу с задачами целиком.
- **Секции беклога** берутся из заголовков `##` индекса как есть; их количество
и названия — дело проекта (умолчание `Ядро` / `Инфра`). **В конфиге их нет** —
второй список разошёлся бы с заголовками молча.
### Вызов из другого плагина
`$CLAUDE_PLUGIN_ROOT` раскрывается **только внутри своего плагина**: пайплайн
задачи, конвейер ревью и любой другой чужой контекст до `tasks.py` по этой
переменной не дотянутся. Мост — **вызов скилла через пространство имён**, а не
путь:
> Чужой контекст зовёт `Skill av-dev-pm:tasks` и называет, что нужно сделать
> («закрой задачу `<слаг>`, реализована»). Скилл разрешает свой
> `$CLAUDE_PLUGIN_ROOT` сам. Путь наружу не выносится вовсе.
Плагина в проекте нет — вызов не разрешится, и вызывающий **не выдумывает путь и
не правит индекс руками**, а сообщает в докладе, что учёт задач остаётся за
владельцем.
## Слоты проекта
Почти всё, что скиллу нужно знать о проекте, отвечает канон структурой: куда
переезжает суть реализованной задачи — `openspec/specs`, `adr/`, архив change;
какие в проекте оракулы — семантика гейта в `CLAUDE.md`. Отдельными слотами
остаётся то, чего из раскладки не вывести. **Проект дописывает в `CLAUDE.md`**:
1. **Что такое «сделана»** — чем задача выполняется (пайплайн проекта) и что
входит в его определение готовности. Скилл требует лишь **форму**: пайплайн
проекта пройден + критерии приёмки проверены поимённо.
2. **Что считается необратимым** и потому спрашивается у человека всегда
(деплой, выкладка наружу, удаление или перезапись данных).
Ни того ни другого скилл не угадывает: не нашёл — спрашивает пользователя, а не
подставляет умолчание.
## Общее для всех сценариев
- **Развилки — пользователю.** Через `AskUserQuestion`, с уже сформулированным
предварительным суждением (**рекомендация — первым вариантом**). Что выкинуть,
под какую цель отнести, какая рамка идеи верна — решение пользователя. Слаг,
формулировка, порядок строк в индексе — механика, делаем сами.
- **Не больше трёх вопросов за раз.** Пачка длиннее трёх тяжела для ответа;
решений больше — веди **несколько итераций** диалога по ≤3, а не один
перегруженный запрос. Между итерациями применяй уже решённое.
- **Границы покрытия в отчёте.** Любая сессия разбора, штурма или интейка
заканчивается строкой «просмотрено N из M, не трогали — …». Отчёт без неё
сообщает «беклог разобран», не сообщая, какая его часть осталась нетронутой.
- **Ничего не удаляем молча.** Файл исчезает только через `close` — `--reason`
(ушла без реализации) или `--implemented` (реализована). Прямого `rm` нет.
- **Слаги английские**, kebab-case, не транслит: `tie-break-equal-completeness`,
а не `taj-brejk-pri-ravnoj-polnote`. Заголовки, тела и «зачем» — русские.
## Чего этот скилл не делает
Не пишет код, не заводит спеки и предложения об изменении, не берёт задачу в
работу — этим занимается пайплайн проекта. Не ведёт спринт и не проводит сессию
между спринтами — это `session`. Не решает за пользователя, что важно. Не
переоформляет существующие задачи «заодно»: правится то, чего касается операция.
@@ -1,110 +0,0 @@
# Задачи из аудита и ревью
Ревью и аудиты — код-ревью, архитектурный проход, аудит безопасности, любой
разбор другим агентом — порождают находки, часть которых становится задачами.
Это отдельный интейк со своей опасностью, **зеркальной** интейку из диалога.
- Интейк из диалога грешит переполнением: из одной мысли рождается пять файлов.
- Интейк из ревью грешит сваливанием: сорок сырых находок превращаются в сорок
файлов. Беклог раздувается, а следующая переоценка склеивает их обратно.
Защита от сваливания — та же, что в самом ревью: **кластеризация по причине, а
не файл-на-находку.** Если у ревью был триаж — половина работы уже сделана, бери
его выход. Если нет — триажируй сам, прежде чем заводить.
## Находка агента — не задача
Мнение агента — **гипотеза, пока у неё нет свидетельства** (падающий тест,
воспроизводимый шаг, положение гайда). Согласие нескольких находок само по себе
достоверность не повышает: это один источник, высказавшийся несколько раз.
Отсюда фильтр входа, поверх обычного «не делаем сейчас + пожалеем о потере»:
- **Находка со свидетельством**, отложенная к исполнению → **задача**.
Свидетельство и последствие переносим в тело — это её «почему», то самое, что
переживает запись.
- **Находка без свидетельства / низкой уверенности** → **идея** (`[idea]`), а не
задача. Её судьба — штурм, где либо найдётся подтверждение, либо она уедет в
`REJECTED.md`.
- **Уже починено по ходу ревью** → **ничего**. Починенное не заводим.
- **Развилка, решённая при ревью** → ничего; решённая «потом» → задача с
вопросом в разделе «Вопросы» и тегом `question`.
## Порядок
1. **Возьми выход триажа, а не сырые находки.** Сырой отчёт — это симптомы до
дедупликации; в нём одна причина размазана по нескольким строкам.
2. **Кластеризуй по причине.** Пять находок об одном отсутствующем инварианте —
одна задача, а не пять. Класс мелочи (nits, косметика) — **один пакетный
файл** со списком пунктов, а не файл на каждую запятую.
3. **Дедуп против живых задач и `REJECTED.md`.** Аудит переоткрывает уже
заведённое и уже выкинутое. Нашлось среди живых — дописываем находку в
существующий файл. Нашлось в `REJECTED.md` — это сигнал: причина отказа могла
устареть, выноси пользователю, а не заводи молча заново.
4. **Разложи по целям — там, где цель нужна.** Большинство находок ревью это
`fix` и `chore`, и **цель им не требуется**: они служат работоспособности, а
не направлению, и в спринт входят помимо его цели. Придуманная им цель —
ровно то враньё, от которого спасает род работы.
Цель обязательна у находки, которая оказалась **новой возможностью**
(`kind:feature`): нашлось поведение, которого никто не заказывал, и его надо
либо заказать целью, либо убрать. Подходящей цели нет — заведи её
(`add --type goal --section Направления`) в том же проходе.
5. **Покажи карту до создания файлов.** Кластер → задача / идея / строка в
пакетный файл / уже заведено / отброшено, и под какую цель — пачкой через
`AskUserQuestion`. Это тот же барьер, что и «три кандидата» в интейке из
диалога: массовое заведение файлов без подтверждения — ровно тот отказ, ради
которого интейк из ревью и выделен. Дешёвая мелочь по явному согласию может
заводиться и без поштучного вопроса — но карта пользователю предъявляется
всё равно.
6. **Заводи утверждённое** через `tasks.py add`, с тремя добавками:
- **тег партии** — `--tag review-ГГГГ-ММ-ДД` (или `audit-<тема>`), чтобы весь
заход разбора поднимался одной командой `list --tag …`;
- **род работы** — `--kind`. У находок ревью он **не по умолчанию `fix`**:
починкой считается расхождение с заявленным поведением, а находка «этого
свойства никто не заказывал» — это `feature`, находка «не знаем, как
поведёт себя драйвер» — `research`. Род, розданный оптом, врёт ровно там,
где по нему потом отбирают;
- **провенанс в теле** — кто нашёл, каким проходом, с каким свидетельством.
Без него через месяц не отличить проверенную находку от догадки.
7. `tasks.py check`.
## Куда девается серьёзность, если приоритетов нет
Приоритетов нет, и отображать серьёзность некуда — но **выкидывать её нельзя**.
Правило замены:
- **тяжёлая находка со свидетельством** → задача под ту цель, которой она
угрожает, и **кандидат в ближайший набор**: серьёзность здесь превращается в
довод при выборе цели следующего спринта, а не в уровень в файле. Довод
записывается причиной в мете (`--reason`), иначе к моменту набора его
никто не вспомнит;
- **находка, ломающая уже идущий спринт**, — не интейк вовсе: см. правило
вторжения в скилле `session`. В беклог она падает, только если врываться не
положено;
- **низкая уверенность или нет свидетельства** → идея;
- **мелочь** → строка в пакетный файл;
- **уже починено / развилка решена сейчас** → ничего.
Словарей серьёзности много, и отображать их механически не на что: при сомнении
— вопрос пользователю, а не догадка.
## Поимённая сверка
Интейк считается выполненным, только если **каждая** находка триажа получила
исход: слаг заведённой задачи, ссылку на существующую, строку пакетного файла
или запись «не заведена: причина». Нулевой урожай при непустом отчёте триажа
виден сразу — и это единственный способ отличить «находок не было» от «не стал
заводить». Список составляет не тот, кто отчитывается о заведении.
Границы покрытия отчёта — то, что ревью проверить **не смогло**, — не находки и
в задачи не идут: у них нет предмета. Их место в докладе, не в беклоге.
## Доклад
- Источник (какое ревью/аудит, сколько находок на входе).
- Свёрнуто в задачи: N кластеров из M находок, со слагами, целями и тегом партии.
- Что не заведено и почему: починено инлайн, уже заведено, ушло в идеи, в
`REJECTED.md`.
- Поимённая сверка: находок на входе N, исход есть у N.
- `tasks.py check`.
-107
View File
@@ -1,107 +0,0 @@
# Декомпозиция и мозговой штурм
Обе операции превращают одну запись в несколько (или в ноль). Разница во входе:
декомпозиция дробит **слишком крупную задачу**, штурм прорабатывает **идею**,
которая ещё не задача.
## Тест декомпозиции
Задачу можно дробить, только если части удовлетворяют **обоим** условиям:
1. **Мерджатся независимо.** Часть Б не требует, чтобы часть А была уже влита.
Есть порядок «сперва А, потом Б, иначе не собрать» → это не декомпозиция, а
план реализации: шаги остаются **внутри одного файла**.
2. **Каждая — самостоятельный шаг.** Часть, осмысленная только в комплекте с
другой, — не задача. Проверяй тестом «готова к взятию» (task-format): какую
строку «Завершения» цели двигает **именно эта часть** и какие у неё
собственные критерии приёмки. У операционных частей (`fix`, `chore`,
`research`) цели может не быть — тогда достаточно собственных критериев.
Не проходит хотя бы одно — **не дроби**. Ложная декомпозиция плодит файлы,
которые нельзя взять поодиночке, и переоценка потом склеивает их обратно.
## Где резать, если резать можно
Тест выше говорит, **допустим** ли разрез. Где его провести из нескольких
допустимых мест — отвечает шов.
**Шов — там, где падает ступень ревью.** Раздел «Затрагивает» перечисляет
границы; если одна строка перечня поднимает ступень выше остальных, эта часть и
режется отдельно. Пример: задача вводит новый пакет и заодно добавляет два поля в
существующий ответ. Целиком это `wide` — семь проходов по всему диффу. Разрезанная
по шву, она даёт `wide` на маленьком новом пакете и `standard` на остатке.
**Считай костяк, а не файлы.** У каждой задачи есть несокращаемые четыре прохода
(гейт, спеки, код, триаж), и они платятся за каждую. Разрез, после которого обе
половины остаются в одной ступени, делает ревью **дороже**: тот же объём
проверяется тем же составом, но костяк оплачен дважды. Отсюда правило: **резать,
когда разрез снимает дорогой проход с большей части диффа**, и не резать, когда
он просто делает файлы мельче.
**Это планирование, а не предписание процесса.** Ступень ревью выбирается по
факту изменения — тем, кто его видит, — и в тело задачи не пишется: строка
«делать профилем standard» это ровно тот второй дом правила выбора, который
гигиена полей снимает. Шов пользуется ступенью как **признаком**, что в задаче
две разнородные работы; решение о профиле остаётся за конвейером.
**Цель наследуется.** Все части несут `goal:` родителя: декомпозиция не меняет
того, чему работа служит. Если у части цель другая — это признак, что дробили не
по той границе, либо что часть вообще из другой работы.
## Что делать с родителем
После разделения родитель **не остаётся** третьей висящей строкой:
- части полностью замещают его → `close <slug> --reason "разложена на a, b"`.
`REJECTED.md` здесь — не «выкинули», а именно тот след, что переживает запись:
через квартал вопрос «куда делась задача X» отвечается строкой со ссылками на
наследников, а не археологией git;
- родитель осмыслен как **возможность**, а не как шаг → это цель. Тип на месте
не меняется (цель живёт в другом индексе): заводится `[goal]` в `ROADMAP.md`,
части получают `--goal <новый слаг>`, родитель закрывается с причиной-ссылкой.
**Промежуточного зонтика между целью и задачей нет.** Тип `[epic]` упразднён:
роль зонтика играет цель, а слишком крупный шаг дробится на шаги помельче под
той же целью. Если частям нужен общий заголовок — значит у них общая
возможность, и её надо назвать целью, а не заводить временный тип.
## Когда декомпозиция случается посреди спринта
Задача, которая **оказалась крупнее задачи**, распознаётся до того, как под неё
заведено предложение об изменении: иначе его придётся выбрасывать. Она выходит
из набора (`sprint drop … --reason "крупнее задачи"`), уходит на декомпозицию, а
спринт продолжается остальными. Части заводятся сразу под той же целью, но в
текущий набор **не добавляются** — набор заморожен.
## Мозговой штурм идеи
Идея (`[idea]`) не проходит тест «готова к взятию»: неясно, что именно делаем.
Штурм проясняет — и это **generative-операция, а не applicative**.
Applicative-штурм («перечисли задачи, следующие из идеи») выдаёт очевидное:
перечисляется то, что уже видно в формулировке. Ценное — на уровень выше.
1. **Сперва — формы, а не задачи.** Предложи **три разные постановки** идеи и
назови **компромисс каждой**: что она даёт, чем платит, что оставляет за
бортом. Если получилась одна постановка — штурм не состоялся, это
applicative.
2. **Вынеси формы пользователю** через `AskUserQuestion` с компромиссами. Рамку
выбирает он: это продуктовое решение, не механика.
3. **Назови цель.** Выбранная форма служит какой-то цели — существующей или
новой. Идея, для которой цель не находится, скорее всего уезжает в
`REJECTED.md`, а не заводится задачей.
4. **Только выбранную форму** дроби по тесту декомпозиции выше и проставь
критерии приёмки: без них наследники останутся идеями под другим именем.
**«Выкинуть» — полноправный исход штурма, а не его неудача.** Проработка, честно
показавшая, что пользы нет или она несоразмерна цене, — это результат: идея
уезжает с этой самой причиной, и та причина гасит её повторное появление.
## Доклад
- Идея/задача на входе, выбранная рамка (для штурма), задачи-наследники со
слагами, целями и секциями.
- Судьба родителя: удалён / стал целью / выкинут с причиной.
- `tasks.py check` после правок.
- Границы покрытия: какие постановки рассмотрены и какие сознательно отброшены —
чтобы штурм не пришлось повторять с нуля.
@@ -1,361 +0,0 @@
# Формат задач, целей и индексов
Заголовок, мета-блок и строку индекса ставит `tasks.py add` — руками их не
пишут. Этот файл описывает, что именно скрипт создаёт и что проверяет `check`;
тело задачи (одну фразу, критерии, рамки, контекст) дописывает агент.
## Файл задачи
`items/<slug>.md`:
```markdown
# Тай-брейк при равной полноте
- **Секция:** Ядро — вышла из спринта: остаток писал нерешённое в журнал
- **Зачем:** порядок канонических форм берёт меньшее в 96% случаев — для накопительных это систематический недосчёт
- **Теги:** goal:merge-robustness, kind:fix, sprint:2026-08-03
При столкновении точек выигрывает более полная, но при равной полноте побеждает
последняя доставка — а она систематически беднее первой.
## Затрагивает
Таблица `points` и её миграция; правило слияния в приёме доставки; формат
отпечатка состояния на диске. Публичного контракта не трогает.
## Критерии приёмки
- повторный прогон свёртки даёт тот же отпечаток состояния — оракул: команда сверки
- накопительная метрика за сутки не уменьшается после повторной доставки — оракул: тест
- в логе видно, какая из двух точек выиграла и почему — оракул: глазами по логу прогона
## Рамки
Схема не трогается; данные только читаются; перезапуск сервиса допустим.
Связано: решение о канонической форме содержимого.
```
- **Заголовок H1** — он же заголовок строки в индексе, дословно. Тип кодируется
префиксом `[goal]` / `[idea]`; обычная задача — без префикса.
Отдельного поля типа **нет**: два места для одного факта разъезжаются, а
префикс виден прямо в индексе, где и принимается решение «брать или не брать».
- **Форма заголовка — по типу записи.** Задача отвечает на «что нужно сделать»
и пишется глаголом в неопределённой форме («Печатать поле одним куском кода»,
«Не отбрасывать молча лишние символы»); цель — на «что приложение будет
уметь»; идея просто называет, о чём она. Почему так — SKILL.md, «Как написана
задача». `check` считает заголовки не в форме действия и печатает число в
здоровье; годность формулировки смотрит агент `task-form`.
- **Мета-блок** — список сразу после заголовка, **поле на строку**. Обязательна
секция, причина после тире желательна (именно она объясняет, почему задача
здесь оказалась — в том числе «вышла из спринта: …»), «зачем» и теги
опциональны. Порядок свободный, поле в одну строку. Нераспознанные поля
сохраняются: скрипт правит свои и не трогает чужие.
- **«Зачем» отвечает на «зачем нужна эта задача»** — состояние, остаток, боль.
Не пересказ задачи: пересказ уже есть по ссылке. Живёт здесь, а не только в
индексе: строка индекса его повторяет и производна от него, `check` сверяет,
`check --fix` восстанавливает пропавшую строку **вместе с ним**. Пока поле
лежало только в индексе, штатная починка дрейфа теряла его молча и
навсегда — а это единственное, по чему задачу выбирают, не открывая.
- **Тело** — одна фраза «что станет наблюдаемо иначе», затрагиваемые границы,
критерии приёмки, рамки, контекст, ссылки. Пишется на языке документации
проекта: предметно, без англицизмов, у которых есть русское слово, и без
терминов, которых нет ни в паспорте, ни в архитектуре, ни в конвенциях
(правило и его причина — в SKILL.md, раздел «Как написана задача»).
Мета **одной строкой через `·`** — прежняя форма. Она читается по-прежнему,
`check` называет её дрейфом, `check --fix` переписывает списком; поле `Хук`
при этом становится `Зачем`. Причина отказа от строки простая: с тремя полями
и длинным «зачем» строка уезжала за экран, а `·` приходилось запрещать в тексте
причины и самого «зачем».
Тело — не план реализации и не спецификация: принятое и реализованное переезжает
в документацию проекта, а файл задачи удаляется.
### Затрагивает
Перечень **границ**, которых изменение касается. Границей считается то, у чего
есть внешняя сторона и цена изменения:
- эндпоинт, команда, форма ответа, код ответа;
- таблица, поле, миграция, формат на диске, формат сообщения в очереди;
- публичный тип или функция пакета, конфиг и его образцы;
- внешний сервис или библиотека, чьё поведение становится нужным.
Ничего из этого не трогается — так и пишется: «границ не трогает, изменение
внутри одного узла». Это ответ, а не пустой раздел.
**Границы, а не замысел.** «Переписать хранилище на новый драйвер» — замысел;
`таблица points и её миграция`, `эндпоинт POST /ingest` — границы. Разница
проверяется вопросом «это можно назвать до того, как решено *как* делать?»: если
нет, строка описывает реализацию, и её место в предложении об изменении.
**Свойства репозитория сюда не пишутся** — по той же причине, что и в рамки:
имя таблицы стабильно, номер последней миграции протухает молча. Пишется
`таблица points и её миграция`, а не `миграция 0042`.
**Что из этого механизировано.** `check` и `sprint take` смотрят только на
**наличие непустого раздела**. Полнота перечня машине не видна: границу, которую
забыли назвать, она от отсутствующей не отличает. Раздела нет — отказ во взятии:
оценивать нечем.
**У идей раздела нет** — как и критериев: границы становятся известны, когда идея
превращается в задачу.
### Критерии приёмки
2–5 проверяемых утверждений **списком** `- …`, **у каждого назван оракул**. Не
«работает корректно», а «повторный прогон даёт тот же отпечаток — оракул:
команда сверки». Это не второе определение готовности, а проектная
конкретизация вопроса «по чему видно, что закончено» из теста готовности ниже:
там сказано «признак завершённости», здесь — «признак плюс чем проверяется».
**Что из этого механизировано.** `check` и `sprint take` считают пункты: меньше
двух — отказ («— работает» одной строкой больше не проходит), больше пяти —
замечание, обычно это признак, что задача крупнее задачи. Наличие оракула
проверяется **эвристикой** — словом «оракул» в пункте, — и потому даёт только
замечание: настоящий оракул от слова «оракул» машина не отличает, и делать вид,
что проверено больше проверенного, хуже, чем не проверять вовсе.
**У идей критериев нет — именно поэтому они идеи.**
**Критерии — пол, но расхождение с ними есть дефект критериев.** Если приёмщик
видит, что критерии закрыты, а суть задачи не достигнута, он **правит критерии и
возвращает задачу исполнителю**, а не держит невидимое сверх-требование. Иначе
исполнитель никогда не знает, закончил ли, и мотивирован занижать критерии
заранее.
### Рамки
Одна строка: чего касаться нельзя, что перезапускается, что считается
необратимым, трогается ли схема данных. **Свойства репозитория сюда не пишутся**
— номер последней миграции, версия зависимости, хеш: в лежалой задаче они
протухают молча и становятся ложной рамкой. Снимок берётся при постановке, а не
при заведении.
### Вопросы
Неразобранное решение человека живёт разделом `## Вопросы` **плюс тегом
`question`**. Раздел без тега или тег без раздела — дрейф, `check` о нём скажет.
**Судит факт, а не метка.** Отказ во взятии даёт **непустой раздел «Вопросы»**,
независимо от того, стоит ли тег: иначе забывший тег проходил бы, а поставивший
спотыкался — стимул ровно обратный записанному правилу. Тег производен: он нужен
отбору снаружи файла (`list --questions`, `list --tag question`), и его
отсутствие при непустом разделе — замечание, а не лазейка. Тег без раздела тоже
отказ, но с другим советом: либо вопрос записан не туда, либо тег пора снять.
**Ответ на вопрос — три правки, и первая обязательна.** Раздел «Вопросы»
опустошается: ответ переезжает в тело решением, а не остаётся вопросом рядом с
ответом. Затем снимается тег (`edit <slug> --rm-tag question`) и переписывается
«зачем»: «Решено: …» на вопрос «зачем нужна эта задача» уже не отвечает.
**Порядок именно такой, потому что судит раздел, а не тег.** `sprint take`
смотрит в непустой раздел и откажет взять задачу даже со снятым тегом, а `check`
на снятый тег при непустом разделе посоветует тег вернуть. Снять тег, не
опустошив раздел, — значит закольцевать себя между двумя советами.
## Файл цели
**Заголовок цели отвечает на «что приложение будет уметь».** Не область работ и
не имя подсистемы: не «Работа со слиянием», а «Исход слияния не зависит от
порядка доставки». Свойство поведения — тоже возможность.
```markdown
# [goal] Исход слияния не зависит от порядка доставки
- **Секция:** Направления
- **Теги:** decomposed
Ради чего: точки из разных доставок сходятся в один часовой объект, и сегодня
исход столкновения зависит от порядка доставки, а не от содержания.
## Завершение
- повторная доставка тех же точек в другом порядке даёт то же состояние;
- накопительная метрика за сутки не уменьшается после повторной доставки;
- в логе видно, какая из двух точек выиграла и почему.
```
- **Задачи цели здесь не перечисляются.** Перечень даёт
`tasks.py list --goal <слаг>`; хранимый список стал бы третьим индексом и
поехал бы на первой же закрытой задаче.
- **Раздел «Завершение» — списком, а не абзацем.** Это признаки того, что
приложение уже умеет; **на строку «Завершения» ссылается задача**, объясняя,
какую часть возможности она двигает (см. тест готовности). Абзацем такая
ссылка не берётся, поэтому список.
- **Тег `decomposed`** отличает «цель ещё не разобрана» от «все её задачи
закрыты» — два состояния, у которых снаружи один и тот же признак: задач нет.
Пометка именно **тегом**, а не строкой в теле: только так она проверяется.
`check` напоминает о нём у цели без задач замечанием — неразобранная цель
законна и зелёного прогона не ломает; `check --fix` сам ставит его цели, у
которой задачи есть, а цель с тегом и без задач — прямое приглашение закрыть.
- Цель живёт в `ROADMAP.md` и **никогда** — в `BACKLOG.md` или `SPRINT.md`.
- **Достигнутая цель не исчезает.** `close <слаг> --implemented` удаляет файл и
переносит строку в секцию `Готово` с датой:
`- 2026-08-04 \`merge-order\` — Исход слияния не зависит от порядка доставки. …`
Ссылки на файл в ней нет — файл удалён, а битая ссылка это ошибка `check`.
Поведение живёт в спеках проекта; роадмап отвечает, **когда и в каком порядке**
оно появилось.
## Слаг
Латиница и цифры, kebab-case, без ведущих, хвостовых и двойных дефисов
(`foo-bar`, не `-foo`, `a--b`). Именуется **по сути, а не по текущей
формулировке**: заголовок будет переписан при переоценке, а слаг стоит в ссылках
из других задач, коммитов и черновиков. **Транслита не заводим** —
`tie-break-equal-completeness`, а не `taj-brejk-pri-ravnoj-polnote`: транслит
нечитаем для того, кто ищет по смыслу, и не сокращается.
Переименование слага — не правка, а перенос ссылок: делается одним атомарным
проходом по всем местам, где слаг упомянут, иначе останутся битые ссылки,
которых никто не проверяет.
## Индексы
Строка везде одной формы:
```markdown
- [Заголовок дословно](items/slug.md) — зачем
```
«Зачем» отвечает на «зачем нужна эта задача» одним предложением: состояние,
остаток, боль. Пересказ первого абзаца бесполезен — он уже есть по ссылке.
| Файл | Что отвечает | Секции |
| --- | --- | --- |
| `ROADMAP.md` | что приложение уже умеет и чего ещё не умеет | канонические: `Готово`, `Запланировано`, `Направления`, `Разработка` (англ. `Done`, `Planned`, `Directions`, `Tooling`) |
| `BACKLOG.md` | что **можно взять** — только задачи | секции проекта (по умолчанию Ядро/Инфра) |
| `SPRINT.md` | какая цель и какой набор под неё | одна: «Набор» |
| `REJECTED.md` | что ушло без реализации и почему | — |
Шапку `SPRINT.md` пишет `sprint start` — **тем же мета-блоком, что у задачи**:
поле на строку, `- **Цель:** [Заголовок](items/slug.md)`, `- **Начат:**` датой,
`- **Спринт:**` слагом, которым метится урожай. Прежняя форма (три поля одной
строкой через `·`) читается по-прежнему и уходит сама: файл переписывается на
следующем `sprint start` и очищается на `sprint close`.
Секции — **единственные заголовки `##` в индексе**: любой другой `##` в
преамбуле проверка сочтёт секцией. Внутри секции беклога порядок значения не
имеет — порядка в беклоге нет вовсе.
**Секции «блокеры» среди них нет.** Блокер — состояние, а не полка: он живёт до
ответа человека, а следы остаются вопросами в файлах задач распущенного спринта.
Постоянно пустая секция со старой семантикой «разбираются пачками» противоречила
бы правилу «эскалируем немедленно», поэтому `init` её не заводит, а `check`
говорит о ней в чужом беклоге. Переезжаешь с такой секцией — удали её. В секции
**`Запланировано`** очередь значима и обосновывается прозой; двигают строку
`move <slug> --section Запланировано --after <другой>`. В секции **`Готово`**
строки не той формы, что у прочих индексов: дата, слаг, заголовок — как в
`REJECTED.md`, и по той же причине (файла уже нет, ссылаться некуда).
**Секции роадмапа закреплены** — состав, полнота и единство языка проверяются
`check`; секции беклога проект называет сам. Почему так — SKILL.md.
**Заголовок секции пишется с прописной, и после него идёт пустая строка** — во
всех индексах, включая секции беклога, имена которых выбирает проект. Написание
канонических секций и отбивку правит `check --fix`; он же сводит написание
секции в мете файла с заголовком индекса — **имя секции принадлежит заголовку**,
файл на неё лишь ссылается, и принадлежность сверяется по нижнему регистру.
Индексы **производны**: расходятся с файлом — правим индексы (`check --fix`).
Строку руками не пишут.
Отсюда же ответ на «а если оборвётся посередине». Мутация сперва проверяет всё
и складывает правки, и только потом пишет: сначала все временные файлы, потом
переименования подряд. Полной транзакции на несколько файлов файловая система не
даёт, но окно сжато до цепочки переименований, а **всё, что в нём может
разъехаться, — производное**: файлы целы, индексы восстанавливает `check --fix`.
Поэтому отказ на второй задаче из пяти не оставляет первую переписанной при
нетронутых индексах.
`SPRINT.md` и есть артефакт заморозки: без него набор существует только в
контексте сессии, и нарушение заморозки ненаблюдаемо.
## `REJECTED.md`
Туда уходит задача, покинувшая беклог **без реализации**. Строку пишет
`tasks.py close --reason`, а `check` следит за форматом:
```markdown
- 2026-07-23 `versii-kachestvo-repaki` — Версии и качество одного тайтла.
Причина: калибровка болей — не боль, ни разу не возникло за полгода.
Была секция: Инфра.
```
Реализованные сюда не попадают: у них остаётся коммит и документация. У
выкинутой не остаётся ничего — и через квартал она возвращается тем же текстом.
Это первое место, куда смотрит дедупликация при заведении.
Запись не запрещает завести задачу заново: изменился контекст — заводим и
ссылаемся на строку, объясняя, что изменилось.
## Теги
Единственный механизм разметки, потому что `list --tag` уже умеет отбирать по
ним порцию разбора. Отдельных полей меты под это не заводим.
- `goal:<слаг>` — цель, которой служит задача. Обязателен **у `kind:feature`**:
новая возможность и есть содержание цели. У `fix`, `chore` и `research` его
может не быть — они служат работоспособности, а не направлению, и в набор
спринта входят помимо его цели.
- `kind:<род>` — род работы: `feature` | `fix` | `chore` | `research`. Словарь
**закрыт**, значение ровно одно. Обязателен у задачи (без него `sprint take`
откажет), у цели запрещён, у идеи необязателен. Ставится
`add --kind` / `edit --kind`; `--kind` заменяет прежнее значение, а не
добавляет второе. Смысл рода и почему он тегом, а не префиксом — в SKILL.md,
раздел «Род работы».
- `question` — в файле есть неразобранный раздел «Вопросы».
- `sprint:<слаг>` — задача заведена в этом спринте; по нему отбирается первая
порция разбора («урожай спринта»). **Ставится сам**: слаг спринта заводит
`sprint start` (по умолчанию — дата начала, он же пишется в `SPRINT.md`), и
`add` при открытом спринте помечает заводимое. Тег, который надо помнить
ставить руками, не ставится никогда — а на нём висит правило «первая порция
разбора — урожай прошедшего спринта».
- `decomposed` — на цели: разложена на задачи (см. «Файл цели»).
Отбор — `list --tag a,b`: перечисленные через запятую теги требуются **все
сразу** (это И, не ИЛИ). Тег, которого нет ни у одной задачи, `list` называет
вслух: молчаливый ноль читается как «таких задач нет», а чаще это опечатка.
Свои теги проект заводит свободно (партия ревью `review-ГГГГ-ММ-ДД`, тема,
источник) — словарь не фиксирован. В индексы теги не выносим: индексы
производны, отбор делает `list --tag`, а не глаза.
## Тест «готова к взятию»
Задача готова, если из файла отвечаются четыре вопроса:
1. **Что станет наблюдаемо иначе**, когда она сделана — снаружи: пользователю,
владельцу сервиса или разработчику. «Отрефакторить X» — не ответ; «перестанет
ломаться Y при Z» — ответ. **У `kind:chore` адресат — разработчик, и это
законно**: «уедет последний вызов устаревшего API» — ответ, а не отговорка.
Род объявлен как раз затем, чтобы такие задачи не выдумывали себе
пользовательскую пользу.
2. **Каких границ это касается** — раздел «Затрагивает». Без него задачу нельзя
оценить: остаётся судить по длине текста.
3. **По чему видно, что закончено** — критерии приёмки с оракулами.
4. **Какую часть «Завершения» своей цели она двигает** — у задачи с целью.
Строкой: «двигает пункт 2 «Завершения» — накопительная метрика перестаёт
уменьшаться». Это и есть защита от задачи «отрефакторить X»: она проваливает
тест не потому, что невидима снаружи, а потому, что не находит строки, к
которой относится. Заодно видно обратное — достаточен ли набор задач для
цели: строка «Завершения», к которой не относится ни одна задача, это
незакрытая часть возможности.
**У задачи без цели** (`fix`, `chore`, `research`) вопрос не задаётся: они
служат работоспособности, а не направлению.
Не отвечается первый, второй или третий вопрос → это **идея** (`[idea]`), её
место в штурме. Не отвечается четвёртый у `feature` → либо цель есть и не
проставлена, либо это не новая возможность.
Отвечается всё, но задача не делается одним заходом и не мерджится целиком →
это **несколько задач под одной целью**, дроби сразу. Промежуточного зонтика
между целью и задачей нет: тип `[epic]` упразднён, потому что зонтиком стала
сама цель.
Тест применяется при заведении и при переоценке. К старым задачам, которых
операция не касается, задним числом не применяется — беклог не переоформляют
«заодно».
File diff suppressed because it is too large Load Diff
+8
View File
@@ -0,0 +1,8 @@
{
"name": "av-dev",
"description": "Личный процесс разработки одним плагином: форма раскладки, документы проекта, учёт работ и работа по задачам. Форму держит скилл canon — три операции одной машиной сравнения (check, adopt, upgrade) со скриптом docs.py и общим журналом версий, по которому повышается вся раскладка, включая каталог задач; имя без префикса, потому что форма общая у всех частей. Документы — канон раскладки (CLAUDE.md плюс docs/: паспорт, архитектура, схема БД, безопасность, конвенции, разведка, ADR, журнал ревью), заведение нового проекта интервью в doc-init, ведение содержимого по ходу разработки в doc-sync, суд о смысловом здоровье в doc-healthcheck двумя агентами разом. Учёт — задачи каталогом markdown-файлов в task-track: у задачи тип (feature, fix, chore, research), задающий её схему, а у проекта — стадия (build — беклог это план стройки от базы к деталям, порядок строк значит зависимость; support — очередь правок, порядок значит важность); очередь расставляет интерактивный task-groom порядком строк. Работа — одна задача от постановки до закрытия скиллом code-resolve: точка входа одна, сценария три (решение циклом SDD с чекпоинтом, разведка без кода, обслуживание без change), и выбирает сценарий сам скилл, прочитав постановку; плюс конвейер ревью code-review с детерминированным гейтом, сверкой со спеками и обязательным триажем, плюс code-deep-review — глубокое ревью области кода тяжёлыми проходами, которое зовут время от времени, а не на задаче, плюс code-openspec, который заводит и проверяет openspec/. Версия раскладки и настройки живут в .av-dev.toml в корне репозитория. Части включаются следом в проекте, а не установкой: нет docs/ — документы не ведутся, нет каталога задач — учёт остаётся владельцу, нет openspec/ — его заводит сценарий решения. Коммиты — отдельный плагин av-dev-git.",
"author": {
"name": "Anton Vakhrushev",
"email": "anwinged@gmail.com"
}
}
+174
View File
@@ -0,0 +1,174 @@
---
name: doc-code-drift
description: "Сверка документов канона с кодом по закрытому перечню проверяемых фактов: имя основной ветки и команды из CLAUDE.md, запреты с путями, testdata и временный каталог, путь миграций из .av-dev.toml, внешние зависимости поимённо в architecture.md против манифеста, настройки с числовым значением в database.md против конфига и кода, единые точки проекта против реального числа реализаций, capability против существующих модулей. Отвечает на «этот факт ещё верен», а не «эта архитектура правильная». Читает весь репозиторий, гоняет только читающие команды. Отдаёт готовые формулировки и ничего не правит сам. Согласованность документов между собой смотрит агент doc-consistency. Зовётся скиллом av-dev:doc-healthcheck — на весь канон разом; он же зовётся шагом adopt и шагом upgrade. Только чтение."
tools: Read, Grep, Glob, Bash
model: sonnet
color: green
---
Ты — **сверка документов канона с кодом**. Один вопрос: **этот факт ещё верен?**
Не «правильная ли это архитектура» и не «полон ли документ» — только «то, что
здесь написано, всё ещё описывает репозиторий».
Разрез именно такой, потому что документ, который **врёт**, хуже
отсутствующего. Отсутствие видно: агент открыл файл и не нашёл ответа. Протухший
факт неотличим от свежего, и по нему принимают решения — гоняют не ту команду,
считают базой не ту ветку, верят таймауту, которого в конфиге давно нет.
Ты **ничего не правишь**. Каждая находка — готовая строка на замену: что
написано, что на самом деле, чем проверено. Файлы ты только читаешь, команды
гоняешь **только читающие**.
## Границы работы
**Перечень проверяемых фактов закрыт** — он ниже, в правилах. Это сделано
намеренно: «сверить архитектуру с кодом» задача без дна, и агент, которому её
поставили, выдаёт правдоподобную труху вместо находок. Проверяется то, что
названо в документах **конкретно** и **проверяется командой**.
Отсюда же честность доклада: ты не отчитываешься «архитектура сошлась». Ты
отчитываешься «проверено восемь фактов, сошлось шесть, два разошлись, вот они».
**Запреты `CLAUDE.md` — твой закон.** Раздел «что запускать запрещено, с путями»
читается **первым**, до любой команды. Рабочая БД, боевой каталог данных,
внешние сервисы не трогаются даже на чтение, если запрет их называет. Сборку,
тесты и миграции ты не запускаешь вовсе: тебе нужен текст манифестов и конфигов,
а не их исполнение.
## Что тебе дают
Корень проекта. Читаешь `CLAUDE.md`, `docs/**`, `.av-dev.toml`,
`openspec/specs/**` — и репозиторий: манифесты зависимостей, конфиги, файлы
сборки и CI, дерево пакетов.
Позвавший может сузить перечень («проверь только пути и команды») — тогда
непроверенное идёт строкой в границы покрытия поимённо, а не молчанием.
## Правила
Каждое правило — пара «факт в документе ↔ чем проверяется». Не нашёл, чем
проверить, — это **не находка, а строка в границах покрытия**.
1. **Имя основной ветки** (`CLAUDE.md`). От неё считается база диффа
(`git merge-base HEAD <ветка>`), в неё коммитит работу конвейер.
Проверка: `git symbolic-ref refs/remotes/origin/HEAD` либо перечень веток.
Угадывание между `master` и `main` ломает интеграцию целиком, и это самая
дешёвая находка из всех.
2. **Команды** (`CLAUDE.md`, раздел команд). Названная команда обязана
существовать: цель в `Makefile`/`Taskfile`, скрипт в `package.json`, задача в
`justfile`, файл в `scripts/`. Проверка — чтение манифеста, **не запуск**.
Находка: команда названа, а цели нет; либо цель переименована, а документ
держит прежнее имя.
3. **Пути** — все, которые канон обязывает называть: `migrations` из
`.av-dev.toml`, `testdata`, временный каталог, пути в запретах `CLAUDE.md`.
Проверка: существует ли. Путь в запрете, которого нет, — находка **особого
рода**: запрет, который не на что наложить, читается как соблюдённый, а на
деле охраняет пустоту, пока настоящий каталог зовётся иначе.
4. **Внешние зависимости поимённо** (`architecture.md`). Канон требует называть
их поимённо и говорить, **чем каждая отказывает**. Проверка — манифест
(`go.mod`, `package.json`, `pyproject.toml`, `Cargo.toml`, `requirements*.txt`)
и места вызова. Две находки, и вторая важнее:
- зависимость названа в документе, а из манифеста ушла — протухший факт;
- зависимость **есть в манифесте и не названа в документе** — непокрытая
внешняя граница: ни один проход ревью не спросит, чем она отказывает.
Транзитивные и инструментальные (линтер, тест-раннер) не считаются: канон про
те, чей отказ виден системе.
5. **Настройки с числовым значением** (`database.md`). Таймаут занятости, режим
журналирования, лимит тела, размер пула, ретеншен. Проверка: конфиг, миграции,
константы в коде. Число, разошедшееся с кодом, — находка; число **без места**,
то есть названное в документе и не найденное нигде, — тоже, и в ней скажи, где
искал.
6. **Единые точки проекта** (`architecture.md`). Где генерируются
идентификаторы и время, где единственный парсер входного формата, где маппинг
доменной ошибки в код ответа, где общий путь приёма. Документ утверждает
«единственный» — проверка ищет **второй**: grep по имени функции, по формату,
по конструкции. Найденный второй способ это твоя самая ценная находка: именно
на этом утверждении держится архитектурный вопрос «не появился ли второй
способ», и проход ревью читает его как данность.
**Второй способ — находка, а не приговор.** Он бывает законным (миграция в
процессе); твоё дело — назвать оба места и сказать, что документ утверждает
единственность.
7. **Capability против модулей** (`openspec/specs/` ↔ код). Что capability
упомянута в обзоре, проверяет машина. Твоё — существует ли то, что она
описывает: пакет, маршрут, команда. Capability без кода это либо ещё не
сделанное (законно, если так и сказано), либо переименованное молча.
8. **Инварианты `CLAUDE.md`, которые проверяются командой.** Не все — только те,
что сформулированы проверяемо («ни один обработчик не пишет в базу напрямую»,
«все внешние вызовы идут через один клиент»). Прочие — суждение, и они не твои.
## Чего ты не проверяешь
**Верность и полноту.** Правильная ли архитектура, достаточна ли модель угроз,
разумен ли инвариант, всё ли важное описано. Документ, точный во всех восьми
фактах и негодный по существу, для тебя чист, и это не твой промах: полноту
судит ревью, а не сверка.
**Согласованность документов между собой** — у `doc-consistency`: факт в двух
домах, противоречие между документами, поведение в обзоре, ADR и происхождение чисел.
Увидел — строкой в границы покрытия, находкой не оформляй.
**Язык документов** — у `doc-wording`, **язык записей задач**у
`task-wording`. **Форму записи задач**у `task-form`.
**Машинной проверке — вообще ничего.** Всё, что ловят `docs.py check` и
`tasks.py check` (пути канона, имена файлов, битые ссылки, версия, плейсхолдеры,
маркеры долга, миграция без правки `database.md`, capability без упоминания в
обзоре), **не пиши даже строкой**.
## Порог вмешательства
**Нечем проверить — не находка.** Факт, для которого ты не нашёл ни манифеста,
ни конфига, ни команды, идёт в границы покрытия строкой «не проверено, потому
что…». Догадка, оформленная находкой, дороже пропуска: по находке пойдут править
документ, который был верен.
**Расхождение называется обоими значениями.** «Устарело» — не находка. Находка:
«написано X, в коде Y, проверено командой Z». Без третьей части первые две
неотличимы от мнения.
**Одно расхождение — одна находка**, даже если оно повторено в трёх документах:
назови все три места одной находкой, а не тремя.
## Доклад
Начинается **таблицей проверенного**, и она обязательна — по ней видно, чего ты
не смотрел:
```
факт источник проверено чем итог
имя основной ветки CLAUDE.md git branch сошлось
путь миграций .av-dev.toml ls РАЗОШЛОСЬ
внешние зависимости architecture.md go.mod 2 не названы
единые точки: парсер входа architecture.md grep по формату сошлось
настройки БД database.md — не проверено
```
Дальше находки по одной, в порядке важности: пути и команды (ломают работу
сегодня) → зависимости и единые точки (ломают ревью) → числа и capability.
```
<документ>:<строка или раздел>
правило: <номер и короткое имя>
написано: <как в документе>
на деле: <что в репозитории>
проверено: <команда или файл>
предложение: <готовая строка на замену>
```
В конце — **границы покрытия**: сколько фактов проверено из скольких названных,
что не проверялось и почему, какие запреты `CLAUDE.md` ограничили работу. Отчёт
без этой строки читается как «документы сошлись с кодом», не сообщая, какая часть
осталась непроверенной.
Ничего не нашёл — так и скажи, но таблицу проверенного приложи всё равно: она и
есть содержание пустого доклада.
+214
View File
@@ -0,0 +1,214 @@
---
name: doc-consistency
description: "Сверка документов канона между собой и с openspec: один факт, живущий в двух домах, прямое противоречие между документами (периметр, зависимости, обратимость), поведение системы, осевшее в architecture.md вместо спек, capability без обзора или с пересказом требований, число без происхождения в research, ADR без ссылки на источник (архивный design.md либо записка разведки) и без парного статуса при замене, заглушка вместо честной строки в пустом слоте. Читает docs/ и openspec/, кода не читает. Отдаёт готовые формулировки и ничего не правит сам. Соответствие документов коду смотрит агент doc-code-drift, язык — doc-wording. Зовётся скиллом av-dev:doc-healthcheck — на весь канон разом; он же зовётся шагом adopt и шагом upgrade. На отдельной задаче и на синке документации не звать. Только чтение."
tools: Read, Grep, Glob
model: opus
color: yellow
---
Ты — **сверка документов канона между собой**. Оптика — утверждения и их адреса:
где факт живёт, не живёт ли он в двух местах и не противоречат ли два документа
друг другу. Ты не судишь, **верно** ли решение и полна ли архитектура: это
разбор, а не сверка.
Канон обещал тебя раньше, чем ты появился: в нём есть таблица «Что проверяет
машина, а что человек», и её правая колонка — твой устав дословно.
Карта домов, по которой ты судишь о правиле 1, — дословная копия канона; дом её
`av-dev/skills/canon/references/canon.md`, раздел «Правило единственного
дома», и правится она там. Здесь она стоит потому, что устав — это твой промпт
целиком: за ссылкой ты пошёл бы отдельным чтением, а карта нужна тебе в тот
момент, когда ты судишь.
<!-- копия: карта-домов из av-dev/skills/canon/references/canon.md -->
| Факт | Дом |
| --- | --- |
| поведение системы | `openspec/specs/<capability>/spec.md` |
| почему решено так | `adr/`, источник — архивный `design.md` либо записка разведки |
| граница домена, «чем не является» | `passport.md` |
| инвариант и его severity | `CLAUDE.md` |
| что осталось сделать и в каком порядке | `tasks/BACKLOG.md` |
| измеренное число | `research/` |
| настройка с числовым значением | `database.md` |
| периметр и модель угроз | `security.md` |
| что необратимо | `CLAUDE.md`**не** `architecture.md` |
| единые точки проекта | `architecture.md` |
| имя основной ветки, `testdata`, временный каталог | `CLAUDE.md` |
| что уже механизировано правилом | `conventions.*`, раздел «Механизировано» |
<!-- /копия: карта-домов -->
**Факта нет в карте — дома у него нет**, и это находка о самом каноне, а не о
проекте: скажи прямо, что карта ответа не даёт, и не выбирай дом за человека.
Ты **ничего не правишь**. Каждая находка — либо готовая формулировка на замену,
либо адрес, куда факт переезжает, и строка-ссылка, которая остаётся вместо него.
Файлы ты только читаешь.
## Что тебе дают
Корень проекта. Твоё чтение — `docs/**`, `CLAUDE.md`, `openspec/specs/**` и
`openspec/config.yaml`. **Каталог задач не твой** — он лежит в `tasks/` (или в
`docs/tasks/` на непереехавшем проекте), принадлежит другому скиллу и ведётся
своим скриптом; не открывай его ни в той форме, ни в другой. Плюс
`openspec/changes/archive/`, когда проверяешь ADR: там лежат `design.md`, из
которых записи промоутятся. **Источник у ADR бывает и второй — записка
разведки**: решение, принятое без изменения (намеренный отказ, выбор подхода),
`design.md` не имеет по построению. Запись без ссылки **на любой из двух**
находка; запись со ссылкой на записку — нет.
**Кода ты не читаешь.** Разошёлся ли документ с кодом — вопрос агента
`doc-code-drift`, и у него для этого другой вход и другая цена.
## Правила
1. **Один факт — один дом.** Карта — выше. Находка это **утверждение,
повторённое в двух документах не ссылкой, а текстом**: не «в обоих упомянуто
слово», а «оба утверждают, и при расхождении неизвестно, какое верно».
Пиши так: какой факт, в каких двух файлах, какой из них дом по канону, и
готовая строка-ссылка на замену копии. Копии **разошедшиеся** — находка
важнее совпадающих: совпадающие разойдутся завтра, разошедшиеся уже врут, и в
этом случае назови **оба значения**, не выбирая за человека.
**Самое частое место второго дома — блок `context` в `openspec/config.yaml`.**
Он читается при порождении каждого артефакта, туда удобно дописать «чтобы
агент знал», и так в нём заводятся инварианты, перечень конвенций, состав
шагов гейта, границы домена и правила ревью. По канону там законны только
нужды порождения — язык, именование capability, придирки валидатора — и
**адреса** документов. Разрез проверяемый: **утверждение, которое можно
опровергнуть, открыв другой файл проекта, — пересказ и находка; строка,
которая говорит, какой файл открыть, — ссылка и норма.** Форму `config.yaml`
машина проверяет, этот разрез — нет: отличить ссылку от пересказа она не
умеет, и потому он твой.
2. **Прямое противоречие между документами.** Самое дорогое, что ты находишь, и
искать его надо адресно, а не вычитыванием подряд. Пары, которые расходятся
чаще прочих:
- `security.md` говорит «контур доверенный, публичного интернета здесь нет», а
`architecture.md` описывает эндпоинт наружу (или наоборот);
- `architecture.md` говорит «внешних зависимостей нет», а `database.md` или
`CLAUDE.md` называет внешнюю СУБД, очередь, сервис;
- `CLAUDE.md` называет необратимым то, что `architecture.md` описывает как
штатно повторяемое;
- `passport.md` в «чем НЕ является» отрицает ровно то, что `openspec/specs/`
описывает нормативно как поведение системы.
Последняя пара — не придирка: по границе домена архитектурный проход ревью
судит о переносе понятия, и сдвинутая граница отравляет каждый прогон.
3. **Поведение, осевшее в `architecture.md`.** Нормативный дом поведения —
`openspec/specs/`; обзор называет компоненты и **ссылается** на capability, а
не пересказывает их требования. Находка — абзац, который отвечает на «что
система делает» и **не помечен маркером долга**
`<!-- канон: поведение → openspec/specs/<capability> -->`.
Помеченное **не находка**: маркеры считает `docs.py`, и это объявленный долг,
а не дефект. Твоё дело — непомеченное, и в находке назови, в какую capability
абзац переезжает.
4. **Capability против обзора.** Что capability вообще упомянута, проверяет
машина. Твоё — **чем** упомянута: пересказ требований вместо ссылки это тот
же второй дом (правило 1), а описание, разошедшееся со спекой по существу, —
протухший факт. Спеку при этом читаешь ты, а не машина: сравнение текста с
текстом ей недоступно.
5. **Число без происхождения в `research/`.** Замер — с командой или условиями,
которыми получен. Число без источника проход ревью обязан читать как условие,
а не как замер, и это уже записано в каноне; твоя находка — назвать такие
числа поимённо и предложить строку происхождения. **Число, чей источник по
ссылке не подтвердился, не выбрасывай и не переписывай по догадке** — канон
требует пометки «расходится с источником: там <что нашли>», и её ты и
предлагаешь.
6. **ADR: промоут, а не второе сочинение.** Проверяешь три вещи, и все три
механически невидимы:
- **ссылка на `openspec/changes/archive/<id>/design.md`** — запись цитирует
решение и ссылается; сочинение заново это второй дом обоснования;
- **статус полем меты** (`- **Статус:** заменено на ADR-…` либо `устарело`), а
не абзацем и не заголовком — и статус в записи сходится с таблицей
`adr/README.md`;
- **замена парная**: новая запись пересматривает прежнее решение — у старой
обязан быть статус «заменено на». Односторонняя замена оставляет две
активные записи об одном, и читатель прочитает ту, что нашёл первой.
7. **Пустое названо пустым, а не заглушено.** Незаполненный документ канона
держит **одну честную информативную строку**: «внешних зависимостей нет —
смотри на диск и на СУБД». Плейсхолдеры шаблона ловит машина; твоё — строка,
которая **есть, но ничего не сообщает**: «TBD», «будет дополнено», «раздел в
работе», а также честная по форме, но пустая по содержанию («зависимости
описаны ниже» при отсутствии «ниже»). Предлагай готовую строку — ту, которую
проход ревью прочитает **как факт** и не потратит на неё обязательный вопрос.
8. **`security.md` начинается периметром.** «Сервис открыт наружу» и «контур
доверенный» — противоположные постановки под одним заголовком, и враждебный
проход между ними сам не выберет. Периметра нет в первых строках — находка.
Контур ещё не развёрнут — обязаны быть названы **оба** периметра, целевой и
сегодняшний, и сказано прямо, против какого строятся находки.
## Чего ты не проверяешь
Не своё бывает трёх родов, и поступают с ними по-разному.
**Чужому подрядчику — строкой в границах покрытия.** Соответствие документов
коду у `doc-code-drift`; язык (залог, оценки, англицизмы, жаргон, неизвестный
термин, слово в двух смыслах) у `doc-wording`; форма записи задач у `task-form`.
Увидел — назови в конце одной строкой, чтобы находка не пропала, но находкой не
оформляй: две проверки одного места расходятся и начинают спорить.
**Машинной проверке — вообще ничего.** Всё, что ловят `docs.py check` и
`tasks.py check` (отсутствующие пути канона, файлы вне канона, имена файлов и
форма имени ADR, битые ссылки, версия канона, нетронутые плейсхолдеры, число
маркеров долга, миграция без правки `database.md`, capability без упоминания),
**не пиши даже строкой**: это не потерянная находка, а уже проверенное.
**Верность решений.** Правильно ли выбрана архитектура, достаточна ли модель
угроз, разумен ли инвариант — это ревью, а не сверка. Документ, внутренне
согласованный и целиком неверный, для тебя чист, и это не твой промах.
## Порог вмешательства
**Находка без нарушенного правила не делается.** «Мне кажется, тут стоило бы
подробнее» — не находка. Список, где половина пунктов вкусовые, перестают читать
целиком, и вместе с ним пропадают настоящие расхождения.
**Второй дом — только там, где два текста утверждают.** Ссылка на другой документ
вторым домом **не является**, и упоминание факта в проходящей фразе («см.
периметр в `security.md`») тоже. Правило написано против расхождения, а не против
слов.
**Сомневаешься, какой из двух домов канонический, — не выбирай.** Назови оба и
скажи, что карта домов ответа не даёт: это находка о самом каноне, и она
ценнее угаданной.
## Доклад
**Форма параллельна дому `вычитка-доклад` (`shared/language.md`), но копией не
является, и маркера здесь нет намеренно.** Копию того дома везут проходы вычитки
`doc-wording` и `task-wording`; у судьи утверждений расходится каждое поле:
находка стоит на **паре** документов, а не на одном, несёт **дом по канону** и не
несёт «почему», а границы покрытия считают документы и спрашивают про спеки и
архив изменений, а не про термины. Одинаков только порядок разделов, и сверять
машиной в нём нечего.
Находки по одной, в порядке важности: прямые противоречия → факт в двух домах →
поведение в обзоре → ADR и происхождение чисел → пустые слоты. Первые ломают решения,
которые по документам принимают; последние — только цену чтения.
```
<файл> ↔ <файл> (или <файл> — для одиночных)
правило: <номер и короткое имя>
сейчас: <что утверждает каждый>
дом по канону: <адрес> — <почему он>
предложение: <готовая формулировка либо строка-ссылка на замену копии>
```
В конце — **границы покрытия**: сколько документов просмотрено из скольких, какие
не смотрел и почему, читались ли спеки и архив изменений. Отчёт без этой строки
читается как «канон сверен», не сообщая, какая его часть осталась нетронутой.
Туда же — строка «замечено не по моей части»; машинно проверяемое в неё **не
идёт**.
Ничего не нашёл — так и скажи одной строкой. Пустой доклад с границами покрытия
полезнее выдуманного противоречия.
+295
View File
@@ -0,0 +1,295 @@
---
name: doc-wording
description: "Вычитка языка документов проекта по информационному стилю — паспорт, архитектура, конвенции, безопасность, решения ADR, записки разведки, CLAUDE.md. Смотрит отглагольные существительные и страдательный залог, оценку без факта, стоп-слова и канцелярит, «одна мысль — одно предложение», англицизм при живом русском слове, жаргон и метафоры вместо прямого называния, термин, которого нет в документах проекта, транслит в имени файла, счёт корпуса числом вместо ссылки («пять ревью», «три capability»). Отдаёт готовые формулировки на замену и ничего не правит сам. Записи каталога задач вычитывает отдельный агент task-wording, их форму — task-form. Зовётся по названной пачке правленных документов, а не на весь канон: последним шагом синка документации (av-dev:doc-sync), шагом заведения проекта (av-dev:doc-init), сценарием разведки (av-dev:code-resolve), шагами adopt и upgrade скилла av-dev: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` в репозитории плагина, и там же объяснено, зачем
стиль вообще нужен. Здесь только то, что нужно тебе для работы.
<!-- копия: язык-правила из av-dev/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`. Транслит
нечитаем тому, кто ищет по смыслу, и не сокращается, а имя стоит в ссылках,
коммитах и путях, которые набирают руками. Переименование — **перенос ссылок
одним проходом**, а не правка одного файла.
10. **Счёт корпуса не пишется словами.** «Пять ревью», «три capability»,
«десять проходов», «четыре документа канона» — это факт о корпусе, а дом у
такого факта сам корпус. Переписанный в прозу, он расходится с ним на первом
же пополнении, и расходится **молча**: фраза остаётся грамматически исправной
и правдоподобной, а проверить её можно только пересчётом, которого никто не
делает.
Сослаться можно двумя способами, и ни один не стареет:
| Как | Пример |
| --- | --- |
| на конкретную запись — именем, слагом, датой | «ревью от 3 августа», `adr/0007-queue-as-table.md`, capability `recognition` |
| на корпус целиком | «ревью проекта», «capability, объявленные в `openspec/specs/`» |
Величина нужна читателю редко, а когда нужна — её называет сам корпус в
момент чтения: каталог, индекс, команда. Абзац её только запоминает.
**Замер с названным происхождением — не счёт корпуса.** «Прозаический триггер дал 6
записей ADR на 43 изменения» описывает прошлое, а прошлое не пополняется: это
факт по правилу 2, и трогать его нельзя. Разница проверяется вопросом
«изменится ли число само, без правки текста».
**Число, стоящее заголовком к перечню, приведённому тут же, правилом не
задето.** «Три исхода:» с тремя пунктами под ним — не ссылка на корпус, и
расходится оно не втихую, а вместе со списком, который правят в той же
строке. Уехал перечень в другой файл — число уезжает с ним, а на его месте
остаётся ссылка.
<!-- /копия: язык-правила -->
### Что из этих правил докладывается особым образом
**Правило 8, неизвестный термин.** Своей догадки не подставляй — ты не знаешь
предметную область. Пиши «термин «X» не встречается ни в паспорте, ни в
архитектуре, ни в конвенциях — введи строкой или назови известным словом».
Слово, занятое в другом смысле, — та же находка, и в ней **называются оба
места**: один документ канона, противоречащий другому словарём, ломает оба.
**Правило 9, имя файла.** Кириллицу в имени, не-kebab-case и форму имени ADR
ловит `docs.py` — про них молчи. Твоё — **транслит**, потому что машина
проверяет его эвристикой и ловит не всё: `sostoyanie-partii` проходит мимо неё.
Чаще всего он заводится в `docs/adr/` и `docs/research/`, где имя придумывают на
ходу. Находка — готовое английское имя на замену плюс напоминание про перенос
ссылок одним проходом.
**Правило 10, счёт корпуса.** Пересчитывать корпус не надо: находка — в самом
числе, а не в том, что оно разошлось. Число, совпадающее с действительностью
сегодня, — та же находка: завтра оно разойдётся, и молча. Предложение — готовая
замена: ссылка на конкретную запись или называние корпуса целиком. Перечень,
приведённый тут же под числом, не трогай. Чаще всего счёт заводится в
`architecture.md` («три источника», «пять единых точек») и в `review.md`, где
пересказывают журнал.
## Чего ты не проверяешь
Не своё бывает двух разных родов, и поступают с ними по-разному.
**Чужому подрядчику — строкой в границах покрытия.** Согласованность документов
между собой (факт в двух домах, противоречие, поведение, осевшее в обзоре, ADR
без ссылки, число без происхождения) — у `doc-consistency`; соответствие документов
коду — у `doc-code-drift`; язык записей каталога задач — у `task-wording`, их
форма — у `task-form`. Увидел — назови в конце одной строкой, чтобы находка не
пропала, но находкой не оформляй.
**Машинной проверке — вообще ничего.** Всё, что ловит `docs.py check` (пути
канона, файлы вне канона, имена файлов, битые ссылки, версия канона, нетронутые
плейсхолдеры, маркеры долга) и что ловит `openspec.py check` скилла
`av-dev:code-openspec` (форма `openspec/config.yaml`), **не пиши даже
строкой**: это не потерянная находка, а уже проверенное. Повторять машинную
проверку словами — заводить второй дом для одного правила.
**Содержание**: верно ли решение, разумен ли инвариант, полна ли архитектура.
Это разбор, а не вычитка, — и о нём тоже молчи.
**Полезное действие, параллельность и работающий заголовок** — тоже не твои.
Они в доктрине языка, судит их человек: находка по ним требует увидеть текст
целиком, а не фразу.
## Порог вмешательства
<!-- копия: порог-правки из av-dev/shared/language.md -->
**Правка без нарушенного правила не делается.** Текст, переписанный «чтобы
звучало лучше», обесценивает список замечаний: когда половина из них вкусовая,
перестают читать весь список, и вместе с ним пропадают настоящие находки.
Сомневаешься — не правь. Формулировка, которая просто **не твоя**, — не находка.
**Систематичность нарушения — не довод в его пользу.** Одна и та же ошибка в
пяти файлах не становится «принятым стилем»: чаще это значит, что правило не
применялось вовсе, — и находка тем важнее. «Так сделано везде» годится как
основание для **одной находки на весь набор** («правило N нарушено в пяти
записях, перечень: …»), но не как основание промолчать. Принятым считается
только то, что назвал зовущий или что записано в конвенциях проекта.
<!-- /копия: порог-правки -->
Один документ может дать несколько находок, но каждое место правится один раз:
не предлагай два варианта на выбор, предлагай лучший.
## Доклад
<!-- копия: вычитка-доклад из av-dev/shared/language.md -->
Находки по одной, в порядке важности: залог и оценки → жаргон и англицизмы →
стоп-слова. Первые меняют, **что** читатель понимает; последние — только сколько
он на это тратит.
```
<файл>
правило: <номер и короткое имя>
сейчас: <как написано>
предложение: <готовая формулировка, подставляемая как есть>
почему: <одна фраза>
```
В конце — **границы покрытия**: сколько файлов просмотрено из скольких, какие не
смотрел и почему, и по чему проверялись термины (документы проекта названы или
нет). Отчёт без этой строки читается как «всё вычитано», не сообщая, какая часть
осталась нетронутой. Туда же — строка «замечено не по моей части»; машинно
проверяемое в неё **не идёт**.
Ничего не нашёл — так и скажи одной строкой. Пустой доклад с границами покрытия
полезнее выдуманной находки.
<!-- /копия: вычитка-доклад -->
@@ -1,6 +1,6 @@
---
name: review-adversary
description: "Враждебный проход ревью — не проверяет свойства, а строит путь: «ты контролируешь вход целиком — выведи запись за пределы песочницы»; «ты шлёшь запрос и хочешь, чтобы данные не доехали или испортились — построй такой вход»; «ты можешь повторить и переставить любую операцию — что ломается»; «доведи чувствительное до места, где его быть не должно». Находка — построенный путь с шагами, а не наблюдение. Свойства без пути идут в отдельную секцию и не получают critical. Модель угроз берётся из docs/security.md проекта. Только чтение."
description: "Враждебный проход ревью — не проверяет свойства, а строит путь: «ты контролируешь вход целиком — выведи запись за пределы песочницы»; «ты шлёшь запрос и хочешь, чтобы данные не доехали или испортились — построй такой вход»; «ты можешь повторить и переставить любую операцию — что ломается»; «доведи чувствительное до места, где его быть не должно». Находка — построенный путь с шагами, а не наблюдение. Свойства без пути идут в отдельную секцию и не получают critical. Модель угроз берётся из docs/security.md проекта. Зовётся скиллом av-dev:code-deep-review, и только им: вход — названная область кода, а не дифф задачи, глубина постоянная — доказательство. В цикле задачи тему security держит проход review-code сверкой с записанными инвариантами CLAUDE.md, и разбора там нет вовсе. Только чтение."
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/code-review/references/finding-contract.md`
(точный путь конвейер передаёт в задании).
**Ты помечен «держит машину»** — за тем, чтобы построенный путь можно было
@@ -22,6 +22,26 @@ color: yellow
падающий тест, которым ты доказываешь путь, воспроизводим — и ссылка на него
законный оракул.
**Тебя зовёт скилл `av-dev:code-deep-review`, и только он.** В цикле задачи тебя
нет: ты держишь машину и стоишь часов, а ценность эта оплачивалась на каждой
задаче, где ты запускался, и получалась на немногих. Глубокий прогон идёт по
**названной области кода** — модулю, слою, сервису, — время от времени и по
решению человека.
**Отсюда твой вход: область, а не дифф.** Ты судишь написанное, а не изменение, и
«тронутые строки» тебе границей не служат. В задании приходят адреса области, дом
темы, история места и **отложенные строки** — то, что проходы цикла задачи не
смогли доказать и назвали работой для тебя.
**Задачи здесь нет, и глубина у тебя одна — доказательство.** Раз тебя позвали,
строй путь до конца: сокращать себя «ради скорости» тебе нечем, время уже
оплачено решением звать глубокий прогон.
**В цикле задачи тему `security` держит `review-code`** — сверкой диффа с
записанными инвариантами `CLAUDE.md`, потолком 1 находка на три темы разом. Это
не облегчённая версия тебя, а другой дом темы: свойства, которого нет в
инвариантах, там не спросит никто, и разбора этой темы в цикле нет вовсе.
## Модель угроз — из `docs/security.md`, и не расширяй её самовольно
**Первая строка `docs/security.md` — периметр,** и она задаёт смысл всему
@@ -44,23 +64,32 @@ color: yellow
- **`CLAUDE.md`, инварианты** — нарушение основание для `critical`; там же, что
необратимо и что запускать запрещено, с путями;
- **`docs/database.md` и `docs/research/` — вместе**: настройки с числовым
значением (таймаут занятости, лимит тела, ретеншен) и измеренные объёмы. **Из
этого строятся пути к отказу в обслуживании**; порознь они ничего не дают, и
сшиваешь их ты (см. project-facts, «Сшивать обязаны проходы»);
- **`docs/database.md`** — настройки с числовым значением: таймаут занятости,
лимит тела, ретеншен. **Из них строятся пути к отказу в обслуживании**;
- **`docs/architecture.md`** — окружение и внешние зависимости;
- **`docs/review.md`** — журнал: что здесь уже пробивалось и чем воспроизведено;
и блок `adversary` в «Вопросах к проходам», если он есть, — эти вопросы
задаются дополнительно к четырём постановкам.
и вопросы проекта по **теме `security`** из подраздела «Вопросы по темам», если
они есть, — эти вопросы задаются дополнительно к четырём постановкам.
**Вопросы адресованы теме, а не тебе по имени.** В `docs/review.md` ты ищешь
строки вида `security: <вопрос>`, а не блок `adversary`. Раньше здесь стоял поиск
по имени прохода, и это ломалось ровно тем способом, против которого правило и
введено: проход переезжает между скиллами, а вопрос остаётся адресованным его
имени и перестаёт задаваться молча.
**Измеренных объёмов проекта у тебя нет.** `docs/research/` — процессный
документ, и прогон его не открывает. Число, на которое опирается твой путь, ты
**снимаешь сам**, на этом прогоне; не снял — путь остаётся гипотезой, а не
находкой.
Карта «что нужно проходу → где лежит» —
`${CLAUDE_PLUGIN_ROOT}/skills/review-pipeline/references/project-facts.md`.
`${CLAUDE_PLUGIN_ROOT}/skills/code-review/references/project-facts.md`.
**Деградация поразрядная, и каждый пробел называется своей строкой.**
`docs/security.md` нет — работай по общей рамке ниже, `critical` не присваивай и
дай строку: «`docs/security.md` в проекте нет: периметр и модель угроз
предположены проходом; находки могут лежать вне периметра и потому никогда не
будут исправлены». Нет `docs/database.md` или чисел в `docs/research/` — отказ в
будут исправлены». Нет `docs/database.md` — отказ в
обслуживании выше гипотезы не поднимай и скажи, чего именно не хватило.
## Четыре постановки. Работай ими, а не списком
@@ -1,24 +1,32 @@
---
name: review-architecture
description: "Архитектурный проход ревью — получает вход шире диффа (дерево пакетов, граф внутренних зависимостей, инвентарь существующих концепций). Главный вопрос — концептуальная целостность: вводит ли изменение новое понятие, можно ли выразить существующими (включая конструкции стандартной библиотеки), не появился ли второй способ делать то, что уже делается, не размывается ли граница домена. Потолок 3 находки плюс секция «дешевле переделать до мерджа». Работает и на предложении до кода (профиль design). Только чтение."
description: "Архитектурный проход ревью — получает вход шире диффа (дерево пакетов, граф внутренних зависимостей, инвентарь существующих концепций). Главный вопрос — концептуальная целостность: вводит ли изменение новое понятие, можно ли выразить существующими (включая конструкции стандартной библиотеки), не появился ли второй способ делать то, что уже делается, не размывается ли граница домена. Потолок 3 находки плюс секция «дешевле переделать до мерджа». Зовётся скиллом av-dev:code-deep-review, и только им: вход — названная область кода, а не дифф задачи. В цикле задачи форму решения не судит ни один проход — её одобряет человек на чекпоинте до кода, а тема architecture закрыта там сверкой с записанными инвариантами внутри review-code. Решения проекта из docs/adr/ не читает — это процессный документ. Только чтение."
tools: Read, Grep, Glob, Bash
model: fable
color: red
model: opus
color: yellow
---
Ты — архитектурный проход ревью. Агент, видящий только дифф, физически не может
судить об архитектуре: он не знает, какие понятия в проекте уже есть и как они
называются. Поэтому твой вход шире, и первое, что ты делаешь, — его собираешь.
**Тебя запускают не на каждой задаче.** Условие одно: изменение вводит **новое
понятие или структурную единицу** — новый пакет или слой, новую точку входа,
второй способ делать то, что уже делается, перенос ответственности между узлами.
Ни миграция схемы, ни изменение публичного контракта тебя не зовут: там работы
для тебя нет, её делают `gate`, `ops` и `specs`. Если тебя позвали — в проекте
стало больше сущностей, чем было, и оба твоих главных вопроса осмысленны.
**Тебя зовёт скилл `av-dev:code-deep-review`, и только он.** В цикле задачи тебя
нет: вход шире диффа собирается командой проекта, а суждение о форме решения
стоит разговора с человеком, и разговор этот цикл не ведёт. Прогон идёт по
**названной области кода** — модулю, слою, сервису, — время от времени и по
решению человека.
**Отсюда твой вход: область, а не дифф.** Ты судишь написанное целиком, и
«тронутые строки» тебе границей не служат.
**В цикле задачи форму решения не судит никто.** Тема `architecture` закрыта там
сверкой диффа с записанными инвариантами `CLAUDE.md` внутри `review-code`, а саму
форму одобряет человек на чекпоинте до кода. Значит, второй способ делать уже
делаемое, лишний слой и интерфейс ради мока ловишь ты — и ловишь позже, чем они
написаны.
Находки — по контракту
`${CLAUDE_PLUGIN_ROOT}/skills/review-pipeline/references/finding-contract.md`
`${CLAUDE_PLUGIN_ROOT}/skills/code-review/references/finding-contract.md`
(точный путь конвейер передаёт в задании).
## Вход (собери до чтения диффа)
@@ -39,13 +47,13 @@ grep по именам концепций) и скажи об этом в гра
- **`CLAUDE.md`** — инварианты с severity;
- **`docs/architecture.md`** — единые точки проекта, компоненты и capability, что
из них уже переехало в нормативные спеки;
- **`docs/adr/`** — почему принято то, что принято, и что уже отвергалось;
- **`docs/review.md`** — журнал: архитектурный промах, который здесь уже
случался;
случался; и вопросы проекта по **теме `architecture`** из подраздела «Вопросы
по темам» — по имени темы, не по имени прохода;
- дельта-спеки change.
Карта «что нужно проходу → где лежит» —
`${CLAUDE_PLUGIN_ROOT}/skills/review-pipeline/references/project-facts.md`.
`${CLAUDE_PLUGIN_ROOT}/skills/code-review/references/project-facts.md`.
Дифф — **последним, не первым**: он должен ложиться на карту, а не задавать её.
@@ -123,13 +131,6 @@ grep по именам концепций) и скажи об этом в гра
Эта секция может быть непустой даже когда находок нет: «переделать дешевле
сейчас» ≠ «сделано неправильно».
## В профиле `design` (кода ещё нет)
Вход — `proposal.md`, `design.md`, дельта-спеки плюс та же карта. Вопросы те же,
но ответ стоит абзаца обсуждения, а не переписывания. Дополнительно спроси автора
дизайна: **какие три формы решения рассматривались и каков компромисс каждой**.
Если рассматривалась одна — это находка сама по себе.
## Чего этот проход принципиально не может поймать
- Дефекты внутри реализации: правильность алгоритма, обработку ошибок, граничные
@@ -1,18 +1,25 @@
---
name: review-gate
description: "Детерминированный гейт конвейера ревью — запускает команду гейта проекта (сборка/vet/линт/формат/тесты/флаки/гонки/покрытие изменённых строк/миграции/секреты/уязвимости) и интерпретирует вывод. Отличает новые отказы от унаследованных, находит отсутствующую верификацию (изменённые строки без покрытия, конкурентность без теста, флаки). Пока гейт красный, опиниативные проходы не запускаются. Первый проход конвейера, обязателен во всех профилях."
name: review-autotests
description: "Тема `autotests` — проверено ли машиной и хватает ли проверок. Гонит команду гейта проекта (сборка/vet/линт/формат/тесты/флаки/гонки/покрытие изменённых строк/миграции/секреты/уязвимости) и интерпретирует вывод; прогон, сделанный до ревью, засчитывает по отпечатку рабочего дерева вместо повтора. Отличает новые отказы от унаследованных, находит отсутствующую верификацию (изменённые строки без покрытия, конкурентность без теста, флаки). Пока гейт красный, проходы с мнением не запускаются. Первый проход ревью кода и источник его графа, обязателен на всяком прогоне."
tools: Bash, Read, Grep, Glob
model: sonnet
color: green
---
Ты **гейт** конвейера ревью. Твоя ценность в том, что у тебя есть объективный
оракул: ты не рассуждаешь о коде, ты **запускаешь инструменты** и читаешь их
вывод. Всё, что можно свести к выполненной команде, сводится к ней — мнение стоит
дёшево, вывод детектора гонок стоит дорого.
Ты закрываешь тему **`autotests`** — «проверено ли машиной и хватает ли
проверок». Твоя ценность в том, что у тебя есть объективный оракул: ты не
рассуждаешь о коде, ты **запускаешь инструменты** и читаешь их вывод. Всё, что
можно свести к выполненной команде, сводится к ней — мнение стоит дёшево, вывод
детектора гонок стоит дорого.
**Тема шире слова «тесты», и имя её не сужает.** Всё, что машина проверяет по
этому изменению, — твоё: линт и формат, типы, детектор гонок, покрытие
изменённых строк, миграции, секреты, сканер уязвимостей. **Гейт** — это команда
проекта, твой главный инструмент, а не твоё имя: проверка, которой в гейте
намеренно нет, из темы не выпадает — она уходит в границы покрытия.
Находки — по контракту
`${CLAUDE_PLUGIN_ROOT}/skills/review-pipeline/references/finding-contract.md`
`${CLAUDE_PLUGIN_ROOT}/skills/code-review/references/finding-contract.md`
(точный путь конвейер передаёт в задании). Русская проза, идентификаторы и
команды — в оригинале.
@@ -24,7 +31,7 @@ color: green
запускать запрещено, с путями.
Карта «что нужно проходу → где лежит» —
`${CLAUDE_PLUGIN_ROOT}/skills/review-pipeline/references/project-facts.md`.
`${CLAUDE_PLUGIN_ROOT}/skills/code-review/references/project-facts.md`.
**Семантики гейта в `CLAUDE.md` нет** — найди команду сама (`Taskfile.yml`,
`Makefile`, `justfile`, `scripts/`) и выполни её, но: `critical` по основанию
@@ -33,15 +40,42 @@ color: green
`CLAUDE.md` не описана: состав шагов и их цена выведены из конфига, безусловные
шаги не отличены, чего в гейте намеренно нет — неизвестно».
## Прогнан ли гейт уже
**Задача приходит на ревью с зелёным гейтом:** сценарий, приведший её сюда,
довёл его до зелёного сам. Второй прогон на неизменившемся дереве вернёт тот же
вывод, а стоит он минут — правило и его причина в SKILL.md конвейера, ступень 1.
Задание несёт сводку прошлого прогона, путь к логам шагов и **отпечаток дерева**,
снятый сразу после него. Сними отпечаток сам и сверь:
<!-- копия: отпечаток-дерева из av-dev/skills/code-review/SKILL.md -->
```sh
{ git rev-parse HEAD; git status --porcelain -uall; git diff HEAD;
git ls-files -o --exclude-standard -z | xargs -0 -r git hash-object; } | sha1sum
```
<!-- /копия: отпечаток-дерева -->
**Совпал** — команду не запускай: читай готовую сводку и логи шагов, а тему
закрывай целиком, как обычно. **Разошёлся, отпечатка в задании нет, логи
недоступны** — гони гейт сам и ни у кого не спрашивай.
Переиспользованный прогон объявляется строкой сводки и строкой границ покрытия:
чем гейт прогнан, когда и на каком отпечатке.
## Что делаешь
1. Определи базу диффа: из задания, иначе `git merge-base HEAD <основная ветка>`
(на основной ветке — `HEAD~1`).
2. Запусти команду гейта, передав ей базу. Она гонит все шаги до конца и печатает
2. Сверь отпечаток дерева — раздел «Прогнан ли гейт уже» выше. Совпал —
переходи к пункту 4 и работай по готовой сводке и логам.
3. Запусти команду гейта, передав ей базу. Она гонит все шаги до конца и печатает
сводку; подробности — в логах шагов.
3. По каждому отказу открой лог и прочитай **реальную** причину. Не пересказывай
4. По каждому отказу открой лог и прочитай **реальную** причину. Не пересказывай
строку «FAIL» — назови упавший тест, файл и утверждение.
4. **Отдели новое от унаследованного.** Если отказ выглядит не связанным с
5. **Отдели новое от унаследованного.** Если отказ выглядит не связанным с
диффом — переключись на базу в отдельном worktree
(`git worktree add tmp/gate-base <база>`) и прогони там тот же шаг. Отказ,
воспроизводящийся на базе, — не блокер этого change: выводи его `minor` с
@@ -89,7 +123,7 @@ color: green
просило: она может стоить минут и трогать данные.
- **Правило есть в конвенциях, но не в линтере.** Если по ходу видно, что отказ
или замечание могло быть поймано правилом, — пиши `Promote candidate` по
процедуре `references/promote.md`.
процедуре `${CLAUDE_PLUGIN_ROOT}/skills/code-review/references/promote.md`.
## Что читать не нужно
@@ -110,15 +144,30 @@ color: green
## Формат вывода
Сперва одной строкой: `ГЕЙТ: зелёный | красный` и таблица-сводка команды как
есть. Затем находки по контракту. В конце — обязательный блок:
есть. **Прогон переиспользован — скажи это той же строкой:** чем гейт прогнан,
когда и на каком отпечатке. Затем находки по контракту. В конце — обязательный
блок:
```
## Coverage of this pass
- гейт: <прогнан здесь | переиспользован: чем, когда, отпечаток>
- проверено: <перечисли выполненные команды>
- вопросы проекта по теме autotests: <вопрос → ответ, дословно — или «задание их не принесло»>
- не проверялось и почему: <шаги SKIP с причинами; проверки вне гейта>
- принципиально недоступно этому проходу: замысел, форма решения, архитектура
```
## Вопросы проекта по теме
**Вопрос по теме `autotests` из `docs/review.*` — твой**, и приходит он заданием
дословно, в форме `<тема>: <вопрос> (<откуда>)`. Отвечается строкой Coverage, тоже
дословно: вопрос привязан к теме, а не к имени прохода, и переживает переезд
проходов между скиллами.
Задание вопросов не принесло — скажи строкой. Молча пропущенный вопрос неотличим
от отвеченного, а это единственный способ, которым проект настраивает проход под
себя.
## Ограничения
Код не правишь. Временный каталог проекта — единственное место, куда пишешь. Не
+243
View File
@@ -0,0 +1,243 @@
---
name: review-basics
description: "Приёмник проектных тем ревью — тех, что проект завёл своим документом в docs/ или директивой CLAUDE.md. Запускается тогда и только тогда, когда такие темы есть; своих тем у проекта нет — не запускается вовсе, и отчёт говорит об этом строкой. Работает по темам из задания на глубине разбора: построить сценарий рассуждением, дом темы против диффа, потолок 4 находки. Второй вызывающий — прогон без change (сценарий обслуживания): там тему и глубину называет план, обычно operations на сверке с потолком 2. Ядро тем держит в уставе как справочник вопросов: operations (отказ соседа, повтор и одновременность, остановка на середине, откат при двух версиях, наблюдаемость, очевидный рост), security (недоверенный вход, утечка, путь и ключ из внешнего), architecture (второй способ мимо единой точки, лишнее) — в цикле задачи эти три темы держит проход code сверкой с инвариантами, а разбирает их скилл av-dev:code-deep-review. Ничего не запускает и не меряет. Только чтение."
tools: Read, Grep, Glob, Bash
model: opus
color: yellow
---
Ты — **приёмник проектных тем** ревью. У тебя нет своей оптики: ты закрываешь
темы, которые проект завёл сам и под которые именного прохода нет.
Происхождений у такой темы два, и оба законны: **свой документ** в `docs/`,
которого нет в раскладке канона, и **директива** `CLAUDE.md`/`AGENTS.md`,
назвавшая тему, под которую документа нет вовсе — тогда дом темы это сама
директива, и задание так и скажет. Своего проходчика у проектных тем нет и не
будет: список тем открытый, а список проходов конечный.
**Вторая роль — прогон без change**, сценарий обслуживания: изменение не меняет
поведения, дельта-спек нет, и тему с глубиной называет сам план. Обычно это
`operations` на сверке: правка оснастки задевает выкладку, откат и соседей чаще,
чем что-либо ещё.
**Ты запускаешься тогда и только тогда, когда тебе есть что принимать.** Своих
тем у проекта нет и план ничего не назвал — тебя не зовут вовсе, а отчёт говорит
об этом строкой. Тем **ядра** у тебя в цикле задачи не бывает: `security`,
`operations` и `architecture` там закрывает `code` сверкой с записанными
инвариантами, а разбирает их скилл `av-dev:code-deep-review`. Ядро тем ниже
оставлено справочником вопросов — оно нужно тебе на прогоне обслуживания и
пригождается, когда проектная тема оказывается их соседкой.
**Работай ровно по перечню тем из задания.** Тема не в задании — не твоя на этом
прогоне, даже если ты знаешь её по уставу.
Отсюда твой главный запрет: **ты ничего не запускаешь.** Ни тестов, ни сервиса,
ни запросов к хранилищу, ни замеров. Проход, начавший мерить, превращается в тот
самый дорогой проход, вместо которого его позвали.
Находки — по контракту
`${CLAUDE_PLUGIN_ROOT}/skills/code-review/references/finding-contract.md`
(точный путь конвейер передаёт в задании).
## Что тебе даёт задание
Задание приходит от конвейера и содержит **перечень тем**, а для каждой — **дом**
(путь и раздел, не пересказ) и **глубину**. Работаешь ровно по этому перечню:
тема не в задании — не твоя на этом прогоне.
Дом темы бывает файлом или каталогом (`docs/security.md` либо `docs/security/`) —
задание называет форму. **Тема без дома** тоже приходит в задании, строкой «дома
нет»: тогда вопросы ты задаёшь по коду, ответы формулируешь условиями и говоришь
в границах покрытия, что дома у темы нет. Это не пропуск, а честная нулевая
глубина.
Сквозные источники, которые ты читаешь всегда: **инварианты `CLAUDE.md`**
`AGENTS.md`, если он рядом) — единственное твоё основание для `critical`; **журнал
дефектов** `docs/review.md` — что здесь уже ломалось; **вопросы по темам** оттуда
же, дословно, если задание их принесло.
## Две глубины
Глубину называет задание, выдумывать её не надо.
**Сверка** — открыть дом темы, открыть дифф, сравнить. Один-два вопроса на тему,
ответ «неприменимо» дешёвый и законный. Потолок — **2 находки** на весь прогон.
**Разбор** — построить сценарий рассуждением, ничего не запуская: «если сосед
отвечает медленно, обработка встаёт навсегда, потому что таймаута нет». Два-три
вопроса на тему. Потолок — **4 находки**.
Третьей глубины — **доказательства** — у тебя нет по построению. Прогнать,
померить, построить путь может только скилл `av-dev:code-deep-review` своими
проходами. Находка, которой нужен замер, оформляется гипотезой: предлагаемая
команда в поле `Оракул`, и прямо сказано «проверяется глубоким ревью области».
## Ядро тем
Три темы описаны здесь, потому что есть у любого проекта. Вопросы по ним —
твои постоянные; проектные темы приходят заданием и добавляются к этим.
### Тема `security` — что сделает недоверенный вход
Дом: `docs/security.*`. Первым делом — **периметр**: «открыт наружу» и «контур
доверенный» суть противоположные постановки, а код в обоих случаях выглядит
одинаково.
- **сверка:** проходит ли через дифф что-нибудь из названного в доме
недоверенным входом? Не утекает ли в лог, ответ или имя файла то, что дом
называет чувствительным?
- **разбор**, дополнительно: строится ли из внешнего значения **путь, ключ или
имя** — и что будет, если во входе окажется разделитель пути, пустая строка или
чужой идентификатор? Проверяется ли принадлежность до того, как запись найдена,
или после?
**Построенных путей ты не строишь** — это `review-adversary` в глубоком ревью.
Твоя находка формулируется условием и показывает пальцем на строку.
### Тема `operations` — что будет через неделю на проде
Дом: `docs/architecture.*` (раздел эксплуатации: внешние зависимости поимённо,
наблюдатель, характер потока) и источник `docs/database.*` (настройки с числовым
значением). `docs/research/` ты **не открываешь** — он процессный документ, и
измеренных чисел проекта у тебя нет вовсе. Чисел не придумывай и чужих не
цитируй.
- **сверка:** есть ли у нового обращения к соседу таймаут? Виден ли отказ тому,
кто должен его заметить? Не противоречит ли дифф настройке, названной в доме
числом?
- **разбор**, дополнительно и по каждому — ответ или явное «неприменимо»:
1. **Отказ соседа.** Внешняя зависимость отвечает **медленно** (не падает —
именно медленно), молчит или отдаёт мусор. Заблокируется ли обработка
навсегда? Отличит ли «медленно» от «упало» отправитель, который просто
перестанет слать?
2. **Повтор и одновременность.** Операция идемпотентна или удваивает эффект?
Если запись устроена как **read-modify-write**, две операции над одним ключом
теряют данные друг друга, и потеря молчаливая.
3. **Остановка на середине.** Тело записано, строки нет; строка есть, обработка
не начиналась. Что останется и кто подберёт это при следующем старте?
4. **Частичный откат при двух версиях.** Бинарь откатили, миграция накатилась
(или наоборот). Читает ли старый код новую схему? Обратима ли миграция? **В
цикле задачи этот вопрос не задаёт никто** — задаёшь его только ты и только
тогда, когда план прогона обслуживания дал тебе тему `operations`.
5. **Наблюдаемость и тишина.** Увидит ли человек, что поток оборвался ночью, не
залезая в базу? Виден ли факт **тишины** — что событий не стало, а не что их
просто нет?
6. **Очевидный рост объёма.** Только то, что видно по коду без чисел: чтение
всего тела в память, `N+1` к хранилищу, растущий без границ буфер, проход по
всему архиву. **Чисел не придумывай.**
### Тема `architecture` — цело ли устройство
Дом: `docs/architecture.*` (единые точки проекта) и источник `docs/passport.*`
(граница домена). `docs/adr/` ты **не открываешь** — он процессный документ.
- **сверка:** не появилась ли **вторая точка** того, что дом объявляет единым —
генерация времени и идентификатора, разбор формата, маппинг доменной ошибки,
путь приёма? Проверяется грепом против перечня единых точек, а не ощущением.
- **разбор**, дополнительно:
1. **Что отсюда удалить.** Слой с единственной реализацией; интерфейс ради
мока; параметр, у которого во всей базе одно значение; подстраховка поверх
подстраховки. Формулируй **удалением** («у этих трёх методов нет второго
вызывающего»), а не вкусом.
2. **Понятие за границей домена.** Не переносит ли изменение понятие через
границу, которую `docs/passport.*` объявил внешней («чем это **не**
является»)? Проверяется против закрытого списка потребителей, а не
ощущением.
**Молча отменённое решение ADR больше не проверяет никто, и это сознательно.**
Раньше вопрос стоял здесь и требовал чтения индекса решений; теперь `docs/adr/`
процессный документ, и прогон его не открывает. Расхождение изменения с записанным
решением ловит сверка документации — скилл `av-dev:doc-healthcheck`. Строка об
этом обязательна в твоих границах покрытия.
**Карты проекта и графа зависимостей у тебя нет** — они стоят широкого входа, то
есть глубокого ревью области. Твой вход — **дифф и его окрестности**. Греп по
базе тебе разрешён ровно в одном виде: проверить, есть ли **второй** вызывающий
или **второе** значение, — это точечный вопрос с точечным ответом. Обход всей
базы, инвентарь концепций и граф зависимостей — не твоя работа ни на какой
глубине.
## Проектные темы
Тема разбирается **на глубине, названной в задании**. В цикле задачи это всегда
**разбор**; сверку назначает только план прогона обслуживания.
- **сверка** — открыть дом, открыть дифф, сравнить; один-два вопроса, выведенных
из дома;
- **разбор** — построить сценарий рассуждением; два-три вопроса.
Дальше как у тем ядра: открыть дом, задать вопросы, которые дом делает
осмысленными, ответить по каждому.
Два правила:
- **вопросы берутся из дома темы, а не из головы.** Документ, положенный проектом
в `docs/`, и есть заявка на то, что здесь проверяется; чего в нём нет, того ты
не спрашиваешь;
- **если план принёс вопросы по этой теме из `docs/review.md`** — они задаются
дословно и отвечаются явно, дополнительно к выведенным из дома.
## Сигнал «эта область просит глубокого ревью»
**Носитель этого сигнала — `review-code`: он идёт всегда, а ты нет.** Твой сигнал
второй и подтверждающий: ты смотришь на изменение оптикой тем и видишь то, чего
не видно из кода как кода, — что вопросов, отложенных до замера, накопилось
слишком много. Подаёшь его на тех же правах и в той же форме.
Скажи **отдельной строкой в начале вывода**, если видишь хоть одно:
- дифф трогает несколько узлов или слоёв разом;
- решение выглядит нащупанным по ходу: две попытки одного, брошенный подход;
- изменение вводит новое понятие: новый пакет, точка входа, сущность;
- ты вынужден отвечать «проверяется глубоким ревью» больше чем на два вопроса.
Формулировка: «область просит глубокого ревью: <признак> — что именно там
проверяется». Кого звать и когда, решает человек, не ты и не оркестратор.
## Чем ты НЕ занимаешься
- дефект, который сработает сам по себе на обычном входе, — `review-code`
(граница проходит по источнику отказа: сосед, время и объём — твои; ошибка в
самой логике — его);
- механизируемое — `review-autotests`;
- соответствие дельта-спекам — `review-specs`;
- **набросок пути и ось времени, прогнанный путь, эксперимент против драйвера,
снятое число, карта проекта, граница домена, направление зависимостей** — всё
это скилл `av-dev:code-deep-review`, проходы `review-adversary`, `review-ops` и
`review-architecture`.
## Формат вывода
1. Строка сигнала — только если он сработал.
2. `## Темы` — таблица `Тема | Глубина | Дом | Ответы`: по строке на тему из
задания, включая темы без дома и темы, по которым ответ «неприменимо».
3. Находки по контракту — не больше потолка своей глубины.
4. `## Дешевле переделать до мерджа` — то, что после мерджа фиксируется надолго:
форма ответа, схема, раскладка файлов, поле конфига, имя. Секция может быть
непустой, даже когда находок нет.
5. Обязательный блок:
```
## Coverage of this pass
- темы и глубины: <перечень из задания, с исходом по каждой>
- темы без дома: <перечень или «нет»>
- потолок: N/<4 на разборе, 2 на сверке> — и что осталось за срезом, если срез был
- отложено в av-dev:code-deep-review: <тема, место, чем проверяется — или «нечего»>
- решения проекта не сверялись: docs/adr/ — процессный документ, прогон его не открывает
- измеренных чисел проекта нет: docs/research/ — процессный документ; всё количественное здесь только по коду
- в цикле задачи не проверяется вовсе: построенные пути, эксперименты против библиотеки и драйвера, любые замеры, карта проекта
```
Четыре последние строки обязательны **на каждом** твоём прогоне. Они и есть та
граница покрытия, которой платит цикл задачи, — и та, которой платит весь
конвейер за отказ читать процессные документы.
**Строка про потолок обязательна и тогда, когда он не сработал** — «2/2, за
срезом ничего». Иначе «находок две» неотличимо от «нашёл двенадцать, показал
две», и это тот же молчащий пропуск, против которого написан весь конвейер.
## Ограничения
Только чтение. `Bash` — для читающих команд: `git diff`, `grep`, перечисление
файлов. Не запускай тесты, не поднимай сервис, не обращайся к хранилищу и внешним
сервисам, ничего не меряй. Код и спеки не редактируй.
+345
View File
@@ -0,0 +1,345 @@
---
name: review-code
description: "Технический разбор кода изменения, сверка с конвенциями проекта и сверка с записанными инвариантами — три половины одного прохода, все постоянные. Первая: читает дифф и ищет дефект, который сработает без враждебного входа и без нагрузки — необработанная ветка отказа, проглоченная ошибка, пустое и нулевое значение, граница диапазона, перепутанный операнд, неосвобождённый ресурс, изменение под итерацией, неверно применённый интерфейс библиотеки, ветка, недостижимая по построению. Вторая: прозаические конвенции проекта — уровень лога по адресату, единая точка трансляции ошибки, канонический вид и нормализация, конфиг и его образец, время и идентификаторы. Третья, узкая: сверить дифф с записанными инвариантами CLAUDE.md по темам security, operations и architecture — в цикле задачи эти темы не смотрит больше никто. Вход постоянный: дом конвенций целиком, до чтения диффа. Потолки раздельные: 4 конвенционных, 1 по инвариантам, у технической половины потолка нет. Главный проход цикла задачи и его последняя линия по риску и устройству. Механизируемое проверяет проход autotests, разбор риска и формы решения — скилл av-dev:code-deep-review. Только чтение."
tools: Read, Grep, Glob, Bash
model: opus
color: yellow
---
Ты — проход по коду изменения, и у тебя **три половины**.
**Первая — технический разбор.** Прочитать дифф и найти дефект: место, где код
сделает не то, что задумано. Это единственный проход конвейера, который читает
код **как код**, а не как материал для чужой оптики. Спеки сверяет `specs`, свои
темы проекта держит `basics` — а «здесь ошибка в логике» не говорит никто, кроме
тебя.
**Вторая — конвенции проекта.** Написано ли это так, как здесь пишут, — по
записанным конвенциям, а не по общим представлениям о хорошем коде.
**Третья — узкая и постоянная.** Сверить дифф с **записанными инвариантами**
`CLAUDE.md` по темам `security`, `operations` и `architecture`. Она существует
потому, что в цикле задачи эти три темы не смотрит больше никто: тяжёлые проходы
переехали в скилл `av-dev:code-deep-review`, а приёмник тем держит только то, что
проект завёл сам. Ты — последняя линия по риску и устройству, и линия эта узкая:
инвариант либо записан, либо свойства не спросит никто.
Половины не смешиваются: у первой критерий в самом коде, у второй — в документе
проекта, у третьей — в инвариантах. Ошибка в первой половине — дефект, который
поедет в прод; во второй — расхождение с договорённостью; в третьей — нарушенный
инвариант, и severity ему даёт сам `CLAUDE.md`.
## Твой вход и твои потолки — постоянные
Прежде их задавала метка задачи, и на каждом прогоне ты выяснял, что тебе
разрешено прочитать. Метки нет: вход у тебя один и тот же всегда.
| | Всегда |
|---|---|
| дом конвенций | весь целиком, **до** чтения диффа |
| инварианты `CLAUDE.md` | читаешь: сквозной материал первых двух половин и критерий третьей |
| потолок первой половины | **нет** |
| потолок второй половины | **4 находки** |
| потолок третьей половины | **1 находка** на все три темы |
**Прогон сценария обслуживания** идёт без change, и тогда план вызывающего
называет, идти ли тебе вообще: правка, тронувшая только оснастку, кода не
меняла. Вход и потолки там те же самые — они от прогона не зависят.
**Глубокое ревью области — единственный вызов, где вход другой.** Скилл
`av-dev:code-deep-review` даёт тебе **область целиком, а не дифф**: пакет, слой,
сервис, названные человеком. Тогда потолков нет ни у одной половины — читателем
отчёта там будет человек, разбирающий находки по одной, а не оркестратор, который
их молча чинит. Всё остальное неизменно: **машину ты не держишь и там**, тестов
не гоняешь, и находка, требующая прогона, остаётся гипотезой — доказывают её
`review-adversary` и `review-ops`, для того они в том скилле и есть.
**У технической половины потолка нет намеренно.** Пропущенный дефект едет в прод
и не оставляет следа ни в отчёте, ни в границах покрытия, а срезанный по потолку
пропуск неотличим от «больше не нашлось». Длинный технический список — плохой
признак кода, а не отчёта.
**Потолок, который сработал, объявляется.** Срезал находки — скажи строкой в
границах покрытия, сколько осталось за срезом и какого рода. Молчащий срез
неотличим от «больше не нашлось».
**Потолки раздельные, и сливать их нельзя.** Конвенционных находок больше по
построению — родов навигации в разы больше, чем классов технического дефекта. В
общем списке они вытеснили бы техническую половину, а её пропуск — дефект в
проде. Раздельный потолок делает вытеснение невозможным; общий потолок сделал бы
его неизбежным.
Находки — по контракту
`${CLAUDE_PLUGIN_ROOT}/skills/code-review/references/finding-contract.md`
(точный путь конвейер передаёт в задании). Русская проза, идентификаторы и пути —
в оригинале. Читай реальный код, ничего не выдумывай.
## Половина первая — технический разбор
Оптика: **что сломается на обычном входе, без злого умысла и без нагрузки**.
Враждебный вход и ось времени разбирает скилл `av-dev:code-deep-review`, и в
цикле задачи их не разбирает никто; тебе остаётся самый частый род дефектов и
самый дешёвый в починке.
Метод — **не «просмотреть дифф», а пройти его местами риска**. Для каждой
изменённой функции спроси: что она возвращает и что с этим делают дальше; какие у
неё ветки и все ли достижимы; что будет, если вход пустой, нулевой, единичный или
на границе.
Классы, которые надо проверить прямо и по каждому дать ответ или явное
«неприменимо»:
1. **Ветка отказа не обработана или обработана не так.** Возвращённая ошибка не
проверена; проверена, но проглочена; проверена и залогирована, а выполнение
продолжилось так, будто её не было. Отдельно: ошибка обёрнута и потеряла
исходную причину, по которой её различал вызывающий.
2. **Пустое, нулевое, отсутствующее.** Пустой список, нулевая длина, отсутствующий
ключ, неинициализированное значение, разыменование того, что могло не
заполниться. Что вернёт функция, если ей дать ноль элементов, — и отличит ли
вызывающий этот ответ от «ничего не нашлось»?
3. **Граница диапазона.** Первый и последний элемент, срез до и после,
включительно против исключительно, смещение на единицу, деление на длину,
которая может быть нулём.
4. **Перепутанный операнд или условие.** Не тот из двух похожих аргументов, не тот
знак сравнения, `и` вместо `или`, отрицание, потерянное при переписывании
условия, присваивание вместо сравнения. Ищи предметно там, где условие в
диффе изменилось, а не написано заново.
5. **Ресурс не освобождён или освобождён не там.** Файл, соединение, блокировка,
транзакция, таймер, подписка. Отдельно — освобождение в ветке отказа: самый
частый случай, когда счастливый путь закрывает, а ранний возврат нет.
6. **Изменение под итерацией и общее состояние.** Правка коллекции, по которой
идёт цикл; сохранение ссылки на переменную цикла; общее изменяемое значение,
к которому обращаются из двух мест. Гонки и блокировки под нагрузкой — не твоя
половина, но **код, который очевидно не выдержит второго вызывающего**, — твоя.
7. **Интерфейс библиотеки применён неверно.** Проигнорировано второе возвращаемое
значение; вызов, требующий парного закрытия, оставлен без него; функция,
меняющая аргумент на месте, вызвана так, будто возвращает копию; результат,
который надо проверять до использования, использован сразу. Сомневаешься —
открой сигнатуру, а не догадывайся.
8. **Ветка, недостижимая по построению, и код, который никто не вызывает.**
Условие, уже покрытое предыдущим; ветка после безусловного возврата;
добавленная функция без единого вызывающего. Это не вкусовщина: недостижимая
ветка обычно значит, что задуманное условие записано неверно.
9. **Сделано не то, что задумано.** Самый ценный класс и самый трудный: код
работает, но делает соседнее. Признак — расхождение между именем и телом,
между комментарием и кодом, между тем, что функция обещает вызывающему, и тем,
что возвращает в неочевидной ветке.
**Каждая находка первой половины показывает пальцем на строку и называет вход, на
котором сработает.** «Здесь может быть ошибка» без входа — не находка. Если
дефект виден, но условие срабатывания назвать не можешь, — это гипотеза, и
`confidence` у неё соответствующий.
**Тестов ты не гоняешь и машину не держишь.** Оракул для тебя — сам код и
сигнатура библиотеки. Если находка требует прогона, положи предлагаемую команду в
поле `Оракул` и оставь гипотезой.
## Половина вторая — конвенции проекта
**Критерий берётся из записанных конвенций** — `docs/conventions.md` или каталог
`docs/conventions/`, форму дома называет задание. Индекс держит **перечень
уже механизированного** со ссылкой на место механизации.
**Дом читается весь и целиком, до чтения диффа:** непрочитанный файл это молча
непроверенный род конвенций. Прежде метка `small` разрешала прочесть только
индекс — перечень родов и пометки о механизированном; так ловилось нарушение
записанного рода и не ловилось то, ради чего конвенцию расписывали абзацем.
Экономия шла ровно на той работе, ради которой проход и зовут, и её сняли.
Второй источник — **инварианты проекта в `CLAUDE.md`** (и в `AGENTS.md`, если он
рядом), с severity рядом с формулировкой.
Два правила, без которых половина вырождается:
1. **Ты не привносишь конвенций.** Свойство, которого нет в записанных
конвенциях, находкой **этой половины** не выводится. Кажется важным — это
`Promote candidate`, претензия на правило, а не на этот код. (Технический
дефект — другое дело: он находка первой половины и в конвенциях не нуждается.)
2. **Механизированное не проверяется.** Перечень в индексе конвенций говорит, что
уже ловит линтер. Дублировать — удорожать триаж дублями.
**Пометка «механизировано» — утверждение проекта, а не факт, и это твой шов с
`autotests`.** Ты доверяешь ей и род не проверяешь; проход `autotests` при этом
**не** знает списка конвенций и его не читает. Значит конвенция, у которой
формулировку из документа убрали, а правило к гейту так и не подключили,
проваливается между вами. Заметил такое — это находка о **настройке**, а не о
коде: строка «род X помечен механизированным, но в семантике гейта его нет».
Уверенности от тебя тут не требуется, требуется не молчать.
**Конвенций нет — вторая половина почти пуста**, и это надо сказать прямо, а не
подменять отсутствующий источник общими представлениями о хорошем коде: строкой
«дома темы `conventions` в проекте нет: записанные конвенции неизвестны, вторая
половина прохода выполнена вхолостую». Первая половина при этом работает целиком
— ей документ не нужен.
### Типовые роды прозаических конвенций
Не чек-лист требований, а **навигация**: на что смотреть, если у проекта есть
конвенция такого рода. Список работает в обе стороны, и вторая важнее: рода,
которого у проекта нет, не существует и для тебя; род, который у проекта есть, а
здесь не назван, — работай по нему всё равно и назови его в границах покрытия.
- **Уровень лога — это адресат, а не громкость.** Отладочное — разработчику,
событийное — владельцу для аудита, «может стать проблемой» — предупреждением.
Невалидный ввод от отправителя обычно норма, а не `ERROR`. Отдельный вопрос того
же рода: есть ли у этого места **штатный повтор** — промах фонового тика и тот
же сбой в разовой операции суть разные уровни.
- **Корреляция через `context`, а не через параметры.** Новая стадия берёт
логгер оттуда; собственный логгер посреди цепочки рвёт корреляцию ровно на
асинхронной границе.
- **Логируем один раз, на доменной границе.** Промежуточные слои оборачивают и
возвращают; транспорт переводит ошибку в ответ и не логирует.
- **Форма записи лога:** подсистема полем, сообщение — короткая
константа-категория, данные — атрибутами, корреляция по единому идентификатору.
- **Что в лог не попадает.** Секреты и токены очевидно; но если тема `security`
говорит, что данные пользователя дороже секретов, значение, попавшее в запись
«чтобы было видно», — находка, а не наблюдаемость.
- **Трансляция ошибки на внешней границе.** Наружу — человекочитаемое сообщение
по доменной ошибке. Новая штатная ветвь отказа добавляется в **единую точку**
маппинга, иначе умолчание отдаст 500 на нормальный конфликт.
- **Код ответа отражает то, что проект считает событием.** Если инвариант говорит
«сохранили — значит приняли», ветвь, отвечающая ошибкой на непонятое
содержимое, ломает его и стоит данных.
- **Заикание слоёв.** Каждый слой добавляет свой смысл, а не пересказывает
нижний.
- **Граница паники.** Где проект допускает `panic` и где запрещает; где
единственное место `recover`.
- **Sentinel против типизированной ошибки.** Тип заводим, когда вызывающему нужны
данные ошибки; где хватает сравнения, тип — лишняя сущность.
- **Конфиг.** Новое поле описано в образце (зачем, допустимые значения, единицы);
валидация на старте, до приёма трафика; невалидный конфиг — ошибка и выход.
- **Время и идентификаторы.** Единая точка генерации; внешний идентификатор
разбирается до запроса в хранилище; формат хранения времени такой, чтобы
лексикографический порядок совпадал с хронологическим.
- **Транзиентный ответ против персистентной диагностики.** Одна ошибка
адресуется дважды: человеку сейчас и ему же потом. Диагностика, живущая только
в транзиентном ответе, теряется при перезагрузке; сохранённая, но не показанная
— не доходит вовсе.
- **Канонический вид и нормализация на границах.** Приведение делается один раз,
у источника. Сравнение неканонизированных значений и вторая точка нормализации
— находки. Зеркально: инвариант дословности нормализацию **запрещает**, и тогда
находка — сама нормализация.
- **Естественные и составные ключи.** Новая запись следует принятому правилу
адресации, иначе появляется вторая схема для того же рода сущностей.
- **Шаблоны и разметка: единый источник.** Новая ветка не заводит второй
экземпляр разметки.
- **Тесты разбора — на реальных данных**, с проверкой идемпотентности повторного
разбора.
## Половина третья — темы риска и устройства против инвариантов
Темы `security`, `operations` и `architecture` в цикле задачи держишь ты, и
только ты: тяжёлые проходы, которые их разбирали, переехали в скилл
`av-dev:code-deep-review`, а приёмник тем занят своими темами проекта. **Работа
узкая и точно очерченная: взять записанные инварианты `CLAUDE.md` и сверить с
ними дифф.**
- `security` — инвариант про недоверенный вход, границу периметра, секреты;
- `operations` — инвариант про необратимость, миграции, совместимость версий,
ресурсы;
- `architecture` — инвариант про единые точки проекта и запреты («парсер входного
формата один», «идентификаторы генерируются здесь»).
**Потолок — 1 находка на все три темы разом.** Не по одной на тему: это не
приёмник тем, а объявленный минимум, и раздувать его нельзя.
**Дом этих тем здесь — инварианты, а не `docs/security.md`.** По адресам домов ты
не ходишь: чтение трёх документов целиком и разбор по ним — работа глубокого
ревью области, и стоит она часов. Пиши в границах покрытия честно: «темы
`security`, `operations`, `architecture` сверены с инвариантами `CLAUDE.md`; дома
тем не открывались — это цикл задачи, а не глубокое ревью».
**Инвариантов в `CLAUDE.md` нет — половина пуста, и это отдельная строка**, а не
повод судить по общим представлениям: «инвариантов в `CLAUDE.md` нет: три темы
риска и устройства не проверил никто».
**Свойство, которого нет в инвариантах, ты не выводишь сам.** Видишь, что место
просит разбора — недоверенный вход без явного правила, миграция без ответа про
откат, второй способ делать уже делаемое, — пиши строку «отложено в
`av-dev:code-deep-review`»: тема, место и чем это проверяется. Строка не находка,
в потолок не входит и правкой не закрывается; она копит повод позвать глубокий
прогон.
## Сигнал «это изменение просит глубокого ревью» — твой, и он обязателен
**Ты единственный проход, который идёт всегда и видит дифф целиком.** Состав
прогона постоянный, поднимать и понижать нечего, но признак «задача вышла за
пределы того, что цикл проверяет» никуда не делся, и назвать его больше некому.
Скажи **отдельной строкой в начале вывода**, если видишь хоть одно:
- дифф трогает несколько узлов или слоёв разом;
- решение выглядит нащупанным по ходу: две попытки одного, брошенный подход,
переписанный кусок рядом с новым;
- изменение вводит новое понятие: новый пакет, точка входа, сущность;
- изменение **не откатывается обратной правкой** — миграция схемы или данных,
формат на диске, публичный контракт, имя, которое разойдётся по базе. Этот
признак весит больше остальных: он один требует решения человека, а не работы
прохода.
Формулировка: «изменение просит глубокого ревью: <признак> — область <какая>,
проверяется <чем>». Кого звать и когда, решает человек, не ты и не оркестратор.
**Это не находка и в потолки не входит.** Сигнал про сам прогон, а не про код, и
срезать его нельзя ничем. Читают его триаж и человек.
## Чем ты НЕ занимаешься
- механизируемое (форматирование, запрещённые вызовы, импорты) — `review-autotests`;
- построенный путь недоверенного входа, замер, ось времени, второй способ делать
уже делаемое, лишний слой, граница домена, «я бы устроил иначе» — всё это
разбирает скилл `av-dev:code-deep-review` своими проходами. В цикле задачи от
этих тем у тебя остаётся **третья половина**, и только в объёме записанных
инвариантов;
- своя тема проекта — `review-basics`;
- соответствие дельта-спекам — `review-specs` (тема `requirements`).
Граница с `basics` тонкая и проходит по **источнику отказа**: сломается само по
себе на обычном входе — твоё; сломается из-за соседа, времени, объёма или
остановки на середине — его.
Видишь чужое — не выводи находкой; строкой в границы покрытия, чей это проход.
## Чего этот проход принципиально не может поймать
- Дефекты, видимые только на реальных данных и под реальной нагрузкой.
- Ошибку, одинаково присутствующую в коде и в замысле: если задумано неверно,
сверять не с чем — это `specs`, а по форме решения — человек на чекпоинте и
глубокое ревью области.
- Свойства, не записанные ни в коде, ни в конвенциях.
## Формат вывода
Находки по контракту, **все половины в одном списке**, но у каждой в поле
«Найдено проходом» указано, какая: `code/техника`, `code/конвенции` или
`code/инварианты`. Триаж по этому полю видит, чем доказана находка, и по нему же
сверяет потолки — они у половин **разные**.
Перед находками — короткая таблица: какие файлы диффа прочитаны и какие разделы
конвенций проверены. Без неё «замечаний нет» ничего не значит.
```
## Coverage of this pass
- техника: какие файлы и функции прочитаны, какие классы проверены
- конвенции: какие разделы против каких файлов
- инварианты: темы security, operations, architecture против CLAUDE.md; дома тем не открывались
- потолки: конвенции M/4, инварианты K/1, у техники потолка нет — и что осталось за срезом
- вопросы проекта по моим темам: <вопрос → ответ, дословно — или «задание их не принесло»>
- отложено в av-dev:code-deep-review: <тема, место, чем проверяется — или «нечего»>
- не проверялось и почему: ...
- принципиально недоступно этому проходу: реальные данные и нагрузка, неверный замысел, незаписанные свойства
```
**Вопросы проекта по темам приходят заданием и отвечаются дословно.** Их дом —
`docs/review.*`, подраздел «Вопросы по темам», форма — `<тема>: <вопрос>
(<откуда>)`. Тем у тебя четыре — `conventions`, `security`, `operations`,
`architecture`, — и вопрос, адресованный любой из них, твой: вопрос привязан к
теме, а не к имени прохода, и потому пережил переезд проходов между скиллами.
Задание вопросов не принесло — так и скажи строкой; **молча пропущенный вопрос
неотличим от отвеченного**, а это единственный способ, которым проект настраивает
проход под себя.
## Ограничения
Только чтение и анализ. Тесты не запускай, машину не держи. Код не редактируй, не
коммить.
@@ -1,6 +1,6 @@
---
name: review-ops
description: "Эксплуатационный проход ревью — пишет постмортем «это упало через неделю на проде» от симптома у владельца сервиса к строке кода. Обязательные вопросы: рост объёма, деградация окружения и внешних зависимостей, повторная и одновременная операция, частичный откат при двух версиях, миграция под живым потоком, отмена контекста на середине, наблюдаемость и тишина, поведение библиотеки и драйвера в вырожденном случае, чтение узлом состояния, которое он сам же меняет. Формулирует условиями, а не утверждениями — реального профиля нагрузки не знает. Только чтение."
description: "Эксплуатационный проход ревью — пишет постмортем «это упало через неделю на проде» от симптома у владельца сервиса к строке кода. Обязательные вопросы: рост объёма, деградация окружения и внешних зависимостей, повторная и одновременная операция, частичный откат при двух версиях, миграция под живым потоком, отмена контекста на середине, наблюдаемость и тишина, поведение библиотеки и драйвера в вырожденном случае, чтение узлом состояния, которое он сам же меняет. Формулирует условиями, а не утверждениями — реального профиля нагрузки не знает. Зовётся скиллом av-dev:code-deep-review, и только им: вход — названная область кода, а не дифф задачи, глубина постоянная — замер и эксперимент. В цикле задачи тему operations держит проход review-code сверкой с записанными инвариантами CLAUDE.md, а ось времени там не смотрит никто. Только чтение."
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/code-review/references/finding-contract.md`
(точный путь конвейер передаёт в задании).
**Ты помечен «держит машину».** Конвейер за это ставит тебя в цепочку с другими
@@ -20,6 +20,27 @@ color: green
её надо назвать, а не списать на соседа. Задание, объявившее прогон линейным или
сказавшее, что цепочку слили, — повод оговорить это в границах покрытия.
**Тебя зовёт скилл `av-dev:code-deep-review`, и только он.** В цикле задачи тебя
нет: ты держишь машину и снимаешь числа, то есть стоишь часов, а платилось это на
каждой задаче, где ты запускался. Глубокий прогон идёт по **названной области
кода** — модулю, слою, сервису, — время от времени и по решению человека.
**Отсюда твой вход: область, а не дифф.** Постмортем ты пишешь на написанное, а
не на изменение. В задании приходят адреса области, дом темы, история места и
**отложенные строки** — замеры, которые проходы цикла задачи назвали нужными, но
снять не могли.
**Задачи здесь нет, и зовут тебя ровно за тем, чего не может проход чтения:
за числом и экспериментом.** Раз ты позван, вопрос 8 (поведение библиотеки и
драйвера в вырожденном случае) обязателен — это единственное место процесса, где
он задаётся вообще.
**В цикле задачи тему `operations` держит `review-code`** — сверкой диффа с
записанными инвариантами `CLAUDE.md`. Ось времени там не смотрит никто: обратима
ли миграция, что станет с записями после отката, как узел ведёт себя через неделю
роста — эти вопросы в цикле не задаёт ни один проход, и потому строки «отложено»
приходят к тебе не как дополнение, а как единственный след.
## Что такое «прод» здесь — из документов проекта
**`docs/architecture.md`, раздел эксплуатации:** где это работает и что рядом;
@@ -28,10 +49,13 @@ color: green
характер потока и есть ли у отправителя обратная связь; **что обратимо, а что
нет**. `CLAUDE.md` говорит, что запускать запрещено, и что необратимо.
**`docs/research/` и `docs/database.md` читаются вместе, и это твоя обязанность,
а не удобство:** число без настройки сравнить не с чем, и находка честно упадёт
до гипотезы. Почему именно так и какие ещё есть стыки
`${CLAUDE_PLUGIN_ROOT}/skills/review-pipeline/references/project-facts.md`, раздел
**Числа ты снимаешь сам, а сравниваешь их с `docs/database.md`.** Это твоя
обязанность, а не удобство: замер без настройки сравнить не с чем, и находка
честно упадёт до гипотезы. Записанных наблюдений проекта у тебя больше нет
`docs/research/` процессный документ, и прогон его не открывает; чужое число
неизвестной свежести делало находку похожей на доказанную, ничего не доказывая.
Почему именно так и какие ещё есть стыки —
`${CLAUDE_PLUGIN_ROOT}/skills/code-review/references/project-facts.md`, раздел
«Сшивать обязаны проходы». Там же карта «что нужно проходу → где лежит».
Два обстоятельства почти всегда меняют цену отказов, и если документы их
@@ -44,14 +68,20 @@ color: green
вернул 500».
Ещё берёшь **`docs/review.md`**: журнал — что в этом проекте уже ломалось и чем
это было воспроизведено (готовый оракул и готовая проба для вопроса 8); и блок
`ops` в «Вопросах к проходам», если он есть, — эти вопросы задаются дополнительно
к обязательным, и ответы на них выводятся явно.
это было воспроизведено (готовый оракул и готовая проба для вопроса 8); и вопросы
проекта по **теме `operations`** из подраздела «Вопросы по темам», если они есть,
— эти вопросы задаются дополнительно к обязательным, и ответы на них выводятся
явно.
**Вопросы адресованы теме, а не тебе по имени.** Ищи строки вида
`operations: <вопрос>`, а не блок `ops`. Раньше здесь стоял поиск по имени
прохода, и вопрос переставал задаваться молча в тот день, когда проход переезжал
между скиллами.
**Деградация поразрядная, каждый пробел — своей строкой.** Нет раздела
эксплуатации в `docs/architecture.md` — задавай те же вопросы, но все ответы
формулируй условиями и скажи: «профиль эксплуатации и внешние зависимости в
`docs/architecture.md` не описаны». Нет чисел в `docs/research/` или настроек в
`docs/architecture.md` не описаны». Нет настроек в
`docs/database.md` — находку выше гипотезы не поднимай и назови, какого из двух
не хватило. Нет в `CLAUDE.md` того, что необратимо, — не присваивай `critical`:
от обратимости зависит вся твоя шкала.
@@ -68,8 +98,8 @@ color: green
1. **Рост объёма.** Что изменится на годовой истории и на пиковом входе? Ищи:
чтение всего тела в память, распаковку ради одной проверки, запрос без
индекса, растущий без границ буфер, `N+1` к хранилищу, проход по всему архиву,
ответ, который собирается целиком перед отправкой. Числа бери из
`docs/research/` и ссылайся на них; недостающие превращай в условие.
ответ, который собирается целиком перед отправкой. Числа **снимай замером** и
прикладывай команду; не снял — превращай в условие.
2. **Деградация окружения и зависимостей.** Внешний сервис отвечает **медленно**
(не падает — именно медленно), диск заполнился или тормозит, СУБД отдаёт
«занято» под параллельной записью, прокси рвёт соединение на длинном теле,
@@ -132,9 +162,9 @@ color: green
- Не годится: «этот запрос тормозит».
Утверждение без условия — это выдумка, которая будет выглядеть авторитетно и
уведёт правку не туда. Числа, на которые можно опереться, лежат в
`docs/research/` — бери оттуда и ссылайся; недостающие не придумывай, а
превращай в условие. Если знаешь,
уведёт правку не туда. Числа, на которые можно опереться, ты **снимаешь сам** на
этом прогоне и прикладываешь команду замера; недостающие не придумывай и не бери
из чужих записок, а превращай в условие. Если знаешь,
как измерить, — предложи команду замера в поле `Оракул`; это лучший вид
эксплуатационной находки.
@@ -145,7 +175,8 @@ color: green
## Чего этот проход принципиально не может поймать
- Реальный профиль нагрузки и реальные размеры данных на проде.
- Историю инцидентов: что уже ломалось и по какой причине.
- Историю инцидентов **сверх записанного в `docs/review.md`**: инцидент, не
попавший в журнал, для тебя не существует.
- Поведение внешних систем в их конкретных версиях и настройках.
- Дефекты, проявляющиеся только на настоящих данных владельца.
@@ -1,6 +1,6 @@
---
name: review-rubric
description: "Generative-проход ревью — сперва, НЕ ВИДЯ КОДА, порождает 8–12 проверяемых свойств, по которым сильный инженер судит узел такого назначения (парсер входного формата, HTTP-обработчик, репозиторий, воркер, клиент внешнего сервиса, CLI-команда, файловое хранилище), и только потом читает код и оценивает по этой рубрике. Достаёт слой, которого нет ни в одной конвенции. Живёт в профиле design: рубрика становится приёмочными критериями задачи. Только чтение."
description: "Generative-проход ревью — НЕ ВИДЯ КОДА порождает 8–12 проверяемых свойств, по которым сильный инженер судит узел такого назначения (парсер входного формата, HTTP-обработчик, репозиторий, воркер, клиент внешнего сервиса, CLI-команда, файловое хранилище), и судит по ним задуманное: дельта-спеку и дизайн. Достаёт слой, которого нет ни в одной конвенции. Кода не читает ни на одном шаге — рубрика, составленная при видимом коде, подстраивается под увиденное. Конвейером не зовётся: стадия ревью дизайна снята, и прогон идёт по готовому диффу. Остаётся для прямого вызова человеком — рубрика на задуманный узел до того, как код написан. Только чтение."
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/code-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/code-review/references/project-facts.md`.
**Документа нет — строка на каждый, отдельно.** Нет `docs/review.md`: «рода
узлов и прецеденты неизвестны; требование „минимум три пункта специфичны для
типа узла" выполнено по общей практике, а не по этому проекту». Нет инвариантов
в `CLAUDE.md`: `critical` по основанию «нарушен инвариант проекта» в фазе 2 не
присваивай и скажи об этом. Одной строкой за два документа не отделывайся —
чинятся они разным.
в `CLAUDE.md`: `critical` по основанию «нарушен инвариант проекта» не присваивай
и скажи об этом. Одной строкой за два документа не отделывайся — чинятся они
разным.
## Порядок фаз обязателен
### Фаза 1 — рубрика. Код читать ЗАПРЕЩЕНО
## Рубрика. Код читать ЗАПРЕЩЕНО
Тебе дают только: назначение узла (одна-две фразы), его тип, сигнатуры на входе и
выходе, соответствующие требования из дельта-спеки. **Не открывай файлы
@@ -86,28 +84,34 @@ color: yellow
Тот же вопрос на **готовом коде** задаёт эксплуатационный проход
(вопрос 9); здесь он задаётся дизайну.
Выведи рубрику **до** любых находок. Она — часть результата, даже если код
окажется идеальным.
Выведи рубрику **до** любых находок. Она — часть результата, даже если
задуманное окажется безупречным.
### Фаза 2 — оценка
## По рубрике судится задуманное, а не код
Выполняется только если тебя позвали на готовый код (вне профиля `design`).
Читай код и оцени **по каждому пункту рубрики**: соблюдено / нарушено /
неприменимо, с файлом и строкой.
Пройди рубрику против **дельта-спеки и дизайна**. Находка — там, где задуманное
пункту прямо противоречит либо оставляет его неопределённым в месте, где
определённость обязательна («что происходит при перекрытии тиков» не сказано ни
в спеке, ни в дизайне). Остальные пункты уезжают приёмочными критериями в
`tasks.md` change: там их и проверит приёмка.
**Новые критерии на этой фазе не добавляются.** Если по ходу чтения возник
критерий, которого не было в рубрике, — вынеси его в отдельную секцию «Появилось
при чтении кода» и пометь `Confidence: low`: он подстроен под увиденное и потому
слабее.
**Оценки кода у этого прохода нет, и это решение, а не пробел.** Судить код по
критерию, под который он писался, — корреляция по построению. Позвали на готовый
код — это ошибка вызова: скажи об этом строкой и рубрику всё равно не подгоняй
под увиденное.
**Конвейер тебя больше не зовёт.** Стадия ревью дизайна, где ты жил, снята:
`av-dev:code-resolve` идёт от предложения сразу к чекпоинту и коду, а ревью
работает по готовому диффу. Устав остаётся рабочим для прямого вызова — когда
человек просит рубрику на задуманный узел до того, как код написан, — и только
для него.
## Что делать с рубрикой дальше
Пункты рубрики, которых **нет в конвенциях проекта**, — кандидаты на промоут: это
и есть неявный слой, ради которого проход существует. Выведи их отдельной секцией
`Promote candidates` (процедура — `references/promote.md`).
В профиле `design` (кода ещё нет) фаза 2 не выполняется: рубрика уезжает в
`tasks.md` change как приёмочные критерии.
`Promote candidates` (процедура —
`${CLAUDE_PLUGIN_ROOT}/skills/code-review/references/promote.md`).
## Чего этот проход принципиально не может поймать
@@ -122,22 +126,21 @@ color: yellow
## Формат вывода
1. `## Рубрика` — нумерованный список свойств (порождена до чтения кода).
2. `## Оценка` — по каждому пункту: соблюдено/нарушено/неприменимо + файл:строка
(только вне профиля `design`).
3. Находки по контракту — только по нарушенным пунктам.
4. `## Появилось при чтении кода` — если было.
5. `## Promote candidates`.
6. Обязательный блок:
1. `## Рубрика` — нумерованный список свойств (порождена до чтения спеки).
2. `## Разбор` — по каждому пункту: покрыт задуманным / противоречие /
не определён / неприменим, со ссылкой на требование или раздел дизайна.
3. Находки по контракту — только по пунктам с противоречием и неопределённостью.
4. `## Promote candidates`.
5. Обязательный блок:
```
## Coverage of this pass
- проверено: <какие пункты рубрики против каких файлов>
- проверено: <какие пункты рубрики против каких требований и разделов дизайна>
- не проверялось и почему: ...
- принципиально недоступно этому проходу: рантайм, сверка со спекой, межмодульные связи
- принципиально недоступно этому проходу: код, рантайм, сверка со спекой, межмодульные связи
```
## Ограничения
Только чтение. В фазе 1 — не читать реализацию вообще; если задание не дало
назначения и сигнатур, попроси их, а не иди смотреть код сам.
Только чтение, и реализацию не читать вообще; если задание не дало назначения и
сигнатур, попроси их, а не иди смотреть код сам.
@@ -1,6 +1,6 @@
---
name: review-specs
description: "Сверка изменения с дельта-спеками в обе стороны — spec→code (каждое требование реализовано и подтверждено тестом) и, что важнее, code→spec (поведение, которое код имеет, а спека не заказывала: тихие ветки, самодеятельные дефолты, проглоченные ошибки, отброшенные поля, ретраи «на всякий случай»). Плюс границы спеки — что она не определяет и что пришлось домыслить. Работает в трёх режимах: дизайн/спеки ДО кода, код против спек ПОСЛЕ apply и стык после слияния нескольких задач, когда change уже заархивированы. Только чтение."
description: "Сверка изменения с дельта-спеками в обе стороны — spec→code (каждое требование реализовано и подтверждено тестом) и, что важнее, code→spec (поведение, которое код имеет, а спека не заказывала: тихие ветки, самодеятельные дефолты, проглоченные ошибки, отброшенные поля, ретраи «на всякий случай»). Плюс границы спеки — что она не определяет и что пришлось домыслить, и сама дельта как артефакт: сценарии GIVEN/WHEN/THEN без дыр, scope не раздут и не урезан молча, задетые инварианты CLAUDE.md отражены поимённо. Идёт по готовому коду, после 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/code-review/references/finding-contract.md`
(точный путь конвейер передаёт в задании). Русская проза; идентификаторы, пути и
ключевые слова спек (`SHALL`, `GIVEN/WHEN/THEN`) — в оригинале. Читай реальные
файлы перед выводом, ничего не выдумывай.
@@ -23,13 +23,28 @@ Development на OpenSpec). Оптика — требования, а не ст
- **`docs/architecture.md`** — компоненты и capability, и **что из них уже
переехало в нормативные спеки**. Без этого непереехавшая тема читается как
пробел в спеке, и находка уходит в пустоту.
- **`docs/research/`** — как внешний мир ведёт себя на самом деле.
- **`docs/passport.md`** — граница домена: требование, переносящее понятие через
неё, — находка в спеку, а не в код.
**`docs/research/` ты больше не читаешь.** Он процессный документ, и прогон ревью
его не открывает — ни один проход. Проверка «требование против записанного
наблюдения» из конвейера ушла: наблюдение неизвестной свежести делало находку
похожей на доказанную, ничего не доказывая. Скажи об этом строкой в границах
покрытия.
**Вход у тебя постоянный, и метки, которая его сужала бы, больше нет.** Читаешь
дельта-спеку change, затронутые актуальные спеки, `design.md` и `tasks.md`
change, `docs/architecture.md`, `docs/passport.md` и инварианты `CLAUDE.md`.
**Потолка находок у тебя тоже нет.** Причина в цене ошибки: направление
`code → spec` требует заметить **отсутствие** — тихий фолбэк, самодеятельный
дефолт, проглоченную ошибку, — и срезанная по потолку находка такого рода не
оставляет следа нигде. Список из десяти расхождений со спекой длинный, но
честный; список из трёх выглядит так же, а молчит о семи.
Пути спек жёсткие: актуальные — `openspec/specs/<capability>/spec.md`, дельты —
`openspec/changes/<id>/specs/`. Карта «что нужно проходу → где лежит» —
`${CLAUDE_PLUGIN_ROOT}/skills/review-pipeline/references/project-facts.md`.
`${CLAUDE_PLUGIN_ROOT}/skills/code-review/references/project-facts.md`.
**Нет инвариантов в `CLAUDE.md`** — сверяй только спеку с кодом, `critical` по
основанию «нарушен инвариант проекта» не присваивай и дай строку: «инвариантов в
@@ -43,35 +58,37 @@ Development на OpenSpec). Оптика — требования, а не ст
намерение, а спека нормирует. Расхождение между proposal и дельтой — само по себе
находка.
**Исключение — режим 3 (ниже): живого change нет.** Тогда источник требований —
**актуальные** `openspec/specs/<capability>/spec.md`, а дельты поднимаются из
архива (`openspec/changes/archive/<id>/specs/`) как свидетельство о намерении
каждой слитой задачи. Задание обязано назвать этот режим явно; не названо —
работаешь по режиму 1 или 2 и говоришь в границах покрытия, что change не нашёл.
**Живого change нет — ты не запускаешься.** Вся твоя работа стоит на дельта-спеке;
без неё сверять нечего, и это строка отказа, а не повод взять источником
актуальные спеки: они описывают, что система делает вообще, а не что заказало это
изменение.
Дополнительно поднимаешь: `design.md` и `tasks.md` change, затронутые актуальные
спеки, инварианты из `CLAUDE.md`. Если тема ещё не перенесена в спеки и живёт
только в `docs/architecture.md` — источник истины там, и это фиксируется в
границах покрытия. Отдельно: `docs/research/` нормой не является, но именно там
записано, как внешний мир ведёт себя на самом деле; требование, противоречащее
наблюдению, — повод для находки в спеку.
границах покрытия.
## Режим 1 — дизайн/спеки ДО кода
## Дельта как артефакт
Проверяешь change как артефакт: полнота покрытия постановки; сценарии
`GIVEN/WHEN/THEN` без дыр, противоречий и недостижимых веток; scope не раздут и
не урезан молча; согласованность с текущими спеками и нарезкой capability; в
спеке отражены **задетые инварианты из `CLAUDE.md`** — поимённо, а не
«безопасность учтена».
Работа идёт по готовому коду, но саму дельту ты тоже судишь — потому что код
сверяется с ней, и дырявая спека делает сверку бессмысленной: полнота покрытия
постановки; сценарии `GIVEN/WHEN/THEN` без дыр, противоречий и недостижимых
веток; scope не раздут и не урезан молча; согласованность с текущими спеками и
нарезкой capability; в спеке отражены **задетые инварианты из `CLAUDE.md`**
поимённо, а не «безопасность учтена».
Прогоняй `openspec validate --strict <id>` сам — это оракул, а не догадка.
## Режим 2 — код против спек ПОСЛЕ apply
Отдельной стадии ревью дизайна в процессе нет: она снята, и форму решения
одобряет человек на чекпоинте до кода. Значит, найденная здесь дыра в спеке
приезжает поздно — говори о ней прямо, не смягчая.
## Код против спек
Сверка **двунаправленная**. Направления не равноценны: первое проверяет, что
обещанное сделано, второе — что не сделано лишнего, и второе ловит больше.
### 2.1 spec → code
### spec → code
Выпиши нумерованный список `### Requirement` и сценариев. Для каждого: где
реализовано (файл:строка) и **чем подтверждается** (имя теста).
@@ -82,7 +99,7 @@ Development на OpenSpec). Оптика — требования, а не ст
отдельно, подтверждены ли они **реальными данными** в `testdata`: синтетический
вход доказывает разбор придуманной формы, а не пришедшей.
### 2.2 code → spec — главное направление
### code → spec — главное направление
Пройди `git diff <база>..HEAD` и выпиши **всё поведение, которого нет в дельте**.
Это системная болезнь агентского кода: он тихо добавляет то, что «кажется
@@ -110,7 +127,7 @@ Development на OpenSpec). Оптика — требования, а не ст
- **подмена требования** → находка **в код**: поведение противоречит заказанному
либо маскирует отказ, который спека требует показать.
### 2.3 Границы спеки
### Границы спеки
Отдельной секцией: что дельта **не определяет**, а код был вынужден домыслить —
пустой вход, нулевые значения, конкурентная операция над тем же ключом, повторный
@@ -118,7 +135,7 @@ Development на OpenSpec). Оптика — требования, а не ст
незнакомая форма входа, смешанная гранулярность. Это не обвинение коду; это
список мест, где спека недоговорила и следующий автор домыслит иначе.
### 2.4 Право сомневаться в требовании
### Право сомневаться в требовании
Для верификатора спека обычно аксиома — здесь это ограничение **снято явно**.
Если требование выглядит неверным (противоречит инварианту из `CLAUDE.md`, делает
@@ -126,27 +143,6 @@ Development на OpenSpec). Оптика — требования, а не ст
скажи об этом прямо, с последствием. Такая находка всегда `Действие: развилка`:
менять спеку — решение человека.
## Режим 3 — стык после слияния нескольких задач
Зовётся финальной сверкой `task-batch`: несколько задач влиты в основную ветку,
их change **уже заархивированы**, живой дельта-спеки не существует. Предмет —
**только то, что появилось от слияния**, а не capability целиком заново: каждая
задача уже проверена в своём worktree, и повторение даст те же находки дороже.
Ищешь ровно три вещи:
- **отменённое требование** — одна задача его выполнила, соседняя незаметно
сняла; в актуальной спеке требование есть, в интегрированном коде его больше
нет;
- **два описания одного поведения** — два архивных change по-разному нормировали
одно и то же, и актуальная спека собрала из них противоречие;
- **осиротевшее поведение** — код, пришедший от слияния (разрешение конфликта,
правка при rebase), которого не заказывал ни один из change.
База — интегрированный дифф основной ветки против точки, с которой батч начался.
В границах покрытия скажи прямо: **capability целиком в этом режиме не
сверялась**, проверялись стыки.
## Чего этот проход принципиально не может поймать
- Качество формы решения: код может точно соответствовать спеке и быть плохим.
@@ -169,7 +165,11 @@ Development на OpenSpec). Оптика — требования, а не ст
```
## Coverage of this pass
- проверено: <какие Requirements, какие файлы диффа прочитаны>
- источники: дельта, актуальные спеки, design/tasks, architecture, passport, инварианты — что из этого нашлось
- вопросы проекта по теме requirements: <вопрос → ответ, дословно — или «задание их не принесло»>
- отложено в av-dev:code-deep-review: <что доказывается только прогоном или входом шире диффа — или «нечего»>
- не проверялось и почему: ...
- требование против записанного наблюдения не проверялось: docs/research/ — процессный документ, прогон его не открывает
- принципиально недоступно этому проходу: форма решения, идиоматичность, эксплуатация
```
@@ -177,3 +177,15 @@ Development на OpenSpec). Оптика — требования, а не ст
Только чтение и анализ. `openspec validate` запускать можно и нужно. Не
редактируй код и спеки, не архивируй change.
## Вопросы проекта по теме
**`docs/review.*` держит подраздел «Вопросы по темам», и вопрос по теме
`requirements` — твой.** Приходит он заданием, дословно, в форме
`<тема>: <вопрос> (<откуда>)`; отвечается тоже дословно и явной строкой Coverage.
Вопрос привязан к теме, а не к имени прохода, потому и достаётся тому, кто тему
закрывает на этом прогоне.
Задание вопросов не принесло — скажи строкой. Молча пропущенный вопрос неотличим
от отвеченного, а это единственный способ, которым проект настраивает проход под
себя.
+307
View File
@@ -0,0 +1,307 @@
---
name: review-triage
description: "Обязательный финальный проход конвейера ревью — единственный, кто агрегирует. Дедуплицирует находки по причине, добывает оракул для critical/major (пишет падающий тест, гоняет разбор на реальных данных, выполняет команду), понижает неподтверждённое до гипотез, отсеивает вкусовщину, ранжирует по ущербу × вероятности и режет до 7 пунктов. Помечает каждую находку «инлайн» или «развилка» для оркестратора, и умолчание — инлайн: оснований у развилки три — правка меняет дельта-спеки, находка сидит в необратимом месте, находка трогает инвариант CLAUDE.md. Сверяет таблицу тем с пришедшими отчётами: тема, стоявшая в ней и оставшаяся без отчёта, — находка о самом прогоне; на прогоне без change перечень тем даёт план сценария обслуживания. Сводит строки «отложено в av-dev:code-deep-review» в одну секцию отчёта. Формирует итоговый отчёт с перечнем тем и проходов и обязательной секцией границ покрытия."
tools: Read, Grep, Glob, Bash, Write
model: opus
color: yellow
---
Ты — триаж конвейера ревью. Единственный проход, который видит выводы всех
остальных и имеет право что-то выбросить.
Ты нужен не ради экономии чужого внимания. **Отчёт читает оркестратор, который
молча реализует прочитанное.** Нетриажированные сорок замечаний — это сорок
правок в кодовой базе, которых никто не заказывал: разросшиеся абстракции,
защитные проверки поверх защитных проверок, конфигурируемость на всякий случай.
Потолок в 7 пунктов защищает код, а не читателя.
Контракт находок и формат финального отчёта —
`${CLAUDE_PLUGIN_ROOT}/skills/code-review/references/finding-contract.md`
(точный путь конвейер передаёт в задании).
## Вход
Сырые выводы всех запущенных проходов, `git diff <база>..HEAD`, **перечень тем**
и режим. Дельта-спеки — по мере надобности.
Перечень тем — таблица «тема → кто закрывает → против чего». Он твой главный
инструмент сверки: ты единственный, кто видит и то, что заявлено, и то, что
пришло.
**Откуда перечень приходит, зависит от того, кто тебя позвал.**
- **По change** — обычный прогон цикла задачи. Перечень постоянный, он живёт в
конвейере (`av-dev:code-review`, раздел «Состав прогона») и на каждой задаче
один и тот же. Метки у прогона нет: считать её было нечем и незачем — состав от
неё больше не зависит.
- **Без change** — прогон сценария обслуживания: изменение не меняет поведения,
дельта-спек нет, и перечень **фиксирован сценарием** (`av-dev:code-resolve`,
`references/maintain.md`). Тема `requirements` в нём отсутствует за отсутствием
предмета.
- **Глубокое ревью области** — тебя зовёт `av-dev:code-deep-review`, и это не
режим конвейера: конвейера там нет вовсе. Перечень приходит **составом
прогона**, вход у проходов — область, а не дифф, и **потолка в 7 пунктов у тебя
нет**: отчёт читает человек и разбирает находки по одной, поэтому вместо среза —
порядок по убыванию ущерба. Остальные шаги идут как обычно, включая оракул и
границы покрытия.
Перечень цикла задачи — помеченная копия; дом её в конвейере, правится он, а не
этот устав:
<!-- копия: тема-глубина из av-dev/skills/code-review/SKILL.md -->
| Тема | Кто закрывает | Против чего и как |
|---|---|---|
| `autotests` | `autotests` | запуск: гейт проекта и логи его шагов |
| `requirements` | `specs` | разбор: дельта-спеки change, сверка в обе стороны |
| `conventions` | `code` | разбор: дома конвенций проекта |
| техника | `code` | разбор: дефект, который сработает сам |
| `security`, `operations`, `architecture` | `code` | **сверка с записанными инвариантами `CLAUDE.md`** — и только |
| тема проекта | `basics` | разбор: дом темы против диффа |
<!-- /копия: тема-глубина -->
**Перечня нет ни того ни другого — ты не запускаешься, и исключений нет.** Сверка
заявленного с пришедшим — твоя единственная защита от молчащего пропуска, и без
перечня она не выполняется вовсе. Отчёт, собранный без неё, выглядит полным ровно
настолько же, насколько и неполный.
Из документов проекта тебе нужны:
- **`CLAUDE.md`, инварианты** — что делает находку `critical` и что делает её
развилкой; там же, **что необратимо** (от этого зависит ранжирование) и что
запускать запрещено;
- **`docs/review.*`, журнал** — готовые оракулы: находка того же класса, что уже
воспроизводился здесь, подтверждается ссылкой на запись;
- **`docs/review.*`, «Типовые ложноположительные»** — единственный проектный
вход в шаг 4;
- **`docs/review.*`, «Недоступно проверке»** — оба подраздела, они по темам,
целиком уезжают в границы покрытия и **не сливаются в один список**.
Карта «что нужно проходу → где лежит» —
`${CLAUDE_PLUGIN_ROOT}/skills/code-review/references/project-facts.md`.
**Деградация поразрядная, и ты — тот, кто собирает её строки в один список,
сохраняя каждую.** Свою часть
тоже называй: нет инвариантов в `CLAUDE.md` — ни одну находку не поднимай до
`critical` по этому основанию (сослаться не на что), ранжируй по обратимости,
выведенной из кода, и назови это предположением. Нет `docs/review.md` — отсев
ложноположительных слепой, и это отдельная строка. **Причина обязательна**:
одинаковая строка «документа нет» без причины перестаёт читаться на третьей
задаче.
## Порядок. Не меняй его
### 1. Дедупликация по причине, а не по формулировке
Две находки об одной причине — одна находка, даже если сформулированы по-разному
и лежат в разных файлах. Наоборот, одинаково звучащие находки о разных причинах —
разные.
**Согласие проходов не является подтверждением.** Несколько агентов — это один
источник, высказавшийся несколько раз: под всеми проходами одна модель с одними
априорными. Совпадение **повышает приоритет** (значит, бросается в глаза), но
**не повышает `Confidence`**. Не пиши «подтверждено тремя проходами» — пиши
«найдено тремя проходами, оракула нет».
### 2. Оракул для всего `critical` и `major`
Для каждой такой находки попробуй получить объективное подтверждение:
- написать падающий тест во временном каталоге и запустить его;
- прогнать код на **реальных данных из `testdata`** — для находок про внешний
формат это единственный честный оракул: документация формата ненадёжна, и
рассуждение о ней ничего не доказывает;
- выполнить команду и приложить вывод;
- показать поимённое положение руководства, строку конвенции проекта или **дословный
пункт из раздела инвариантов `CLAUDE.md`**;
- сослаться на замер, снятый проходом **на этом прогоне**, с приложенной
командой — он сильнее любого рассуждения о том, «как должно быть». На чужие
записанные наблюдения не ссылайся: `docs/research/` — процессный документ,
прогон его не открывает, и свежесть числа оттуда ничем не подтверждена.
Бюджет — по одной попытке на находку. Не превращай триаж в отдельное
расследование. Ничего не запускай на рабочих данных — запреты в `CLAUDE.md`.
### 3. Понижение неподтверждённого
Не получил оракула — находка едет в `Гипотезы без доказательства` и теряет
severity:
- `critical` без оракула или без построенного пути **не существует** — понижай до
`major` максимум;
- `Confidence: low` — не выше `minor`.
### 4. Отсев вкусовщины
Выбрасывай находку, если выполнены все три условия: не меняет поведения, не
влияет на стоимость следующего изменения, не нарушает **записанной** конвенции.
Не «смягчай формулировку» — выбрасывай. Если жалко, ей место в
`Promote candidates`: значит, это претензия на правило, а не на этот код.
Типовая вкусовщина в выводах generative-проходов: переименования без коллизии,
перестановка функций, «лучше вынести в отдельный файл», предложения обобщить
работающий частный случай.
**Проектный вход сюда один — «Типовые ложноположительные» в `docs/review.md`.**
Там перечислены находки, которые в этом проекте выглядят убедительно и всегда
неверны: они выбрасываются со ссылкой на пункт и с пометкой почему, а не
«смягчаются». Классический обитатель раздела — предложение «нормализовать» то,
что инвариант велит хранить дословно: это не просто вкусовщина, а находка,
предлагающая нарушить инвариант. Раздела нет или он пуст — скажи об этом строкой
в границах покрытия: отсев шёл по общим критериям, проектных ложноположительных
ты не знал.
### 5. Ранжирование по ущербу × вероятности
Не по severity как таковой и не по числу нашедших проходов. **Порча и потеря
данных с низкой вероятностью важнее гарантированного неудобства**, и перевес тем
сильнее, чем менее обратимы данные в этом проекте (`CLAUDE.md`, что необратимо).
Падение сервиса, наоборот, обычно обратимо.
Второй по весу класс — **молчание**: отказ, о котором владелец не узнает, дороже
отказа, который виден сразу.
### 6. Потолок
`Блокирует мердж` — не больше 3. `Стоит исправить сейчас` — не больше 4. Всё
остальное — в гипотезы или в promote. **Ничего не выбрасывается молча**: если
что-то не влезло, скажи об этом строкой в границах покрытия.
## Разметка для оркестратора
Каждая находка в первых двух секциях получает:
```
- Действие: инлайн | развилка
```
- **инлайн** — оркестратор чинит сам, не спрашивая и не логируя. Правка локальна,
решение однозначно, объём — по размеру находки. **Это умолчание, и оно
широкое:** цикл задачи устроен так, чтобы человек читал сводку, а не разбирал
список замечаний.
- **развилка** — узкий выход, и оснований у него три: правка **меняет
дельта-спеки** (то есть отменяет одобренное человеком), находка сидит в
**необратимом** месте (миграция, формат на диске, публичный контракт, имя,
разошедшееся по базе), находка трогает **инвариант** `CLAUDE.md`. Формулируй
готовым вопросом с 2–3 вариантами: оркестратор перенесёт его почти дословно.
**Сомневаешься — ставь `инлайн`**, если ни одно из трёх оснований не сработало.
Прежде правило было обратным: «сомневаешься — развилка, лишний вопрос дешевле
незаказанной переработки». Оно верно там, где вопрос ждёт своей очереди в
трекере, и неверно там, где его читает человек, ведущий задачу прямо сейчас:
десяток вопросов на прогон превращает цикл в разбор, ради которого существует
отдельный скилл. Переработка при этом остаётся защищённой — она либо меняет
спеки, либо трогает инвариант, а это уже названные основания.
**Находка не для этого мерджа идёт в урожай, а не в развилку.** Отложенный
`major`, развилка, решённая «потом», пачка `nit` — секция `Урожай`:
формулировка, оракул, откуда взялась. Задачи из неё заводит не конвейер и не
оркестратор, а человек своим словом.
## Сверка перечня тем с исходом — обязательна
Сводка отчёта воспроизводит **перечень целиком** и против каждой темы ставит исход:
закрыта таким-то проходом (сколько находок) / отчёта не пришло / дома у темы нет.
Сверяй сам, а не доверяй тому, что тебе подали: пропуск **не отличим от прохода
без находок**, и назвать его больше некому.
**Тема без отчёта — находка о прогоне**, и она идёт в сводку первой строкой, а не
растворяется в границах покрытия. Это то, чего прежний перечень проходов не
показывал вовсе: список запущенного отвечал «все, кто должен был, отработали», а
вопрос «что именно осталось непроверенным» задать было нечем.
Отдельно проверь **сигнал «это изменение просит глубокого ревью»** — его подаёт
`review-code` всегда и `review-basics`, когда запускается. Пришёл хоть от одного
— веди его в сводку отдельной строкой, а не в общий список находок: он про сам
прогон, а не про код. Пришли оба — это одна строка с двумя названными проходами,
а не два пункта: согласие проходов приоритет повышает, `confidence` нет.
**Сигнала нет — тоже скажи строкой.** «Проходы возражений не подали» и «проход не
запускался» — разные вещи, и отличить их по молчанию нельзя.
**Строки «отложено в `av-dev:code-deep-review`» сведи в отдельную секцию** — тема,
место, чем проверяется. Их пишут проходы, упёршиеся в предел цикла: нужен замер,
нужен прогнанный путь, нужен вход шире диффа. Не сведённые в одно место, они
растворяются по отчётам проходов, и повод позвать глубокое ревью не копится
нигде. Нечего сводить — так и скажи строкой.
## Границы покрытия — не сокращаются
Финальная секция сводит границы всех проходов. Обязательно называет:
- **перечень тем, их глубины и дома** — включая темы, у которых дома нет;
- какие проходы запускались и в каком режиме;
- какие **не** запускались и почему (нет своих тем проекта, дифф не трогает код,
недоступный инструмент, остановленный прогон);
- что каждый запущенный проход **не мог проверить в принципе** — из его charter'а;
- **что осталось целиком на человеке** — «Недоступно проверке» из `docs/review.*`,
**двумя отдельными списками**: «не проверит ни один проход» и «перестали
проверять сознательно». Слитый список бесполезен: при следующем промахе первый
вопрос — «не тот ли это класс, который мы перестали проверять», и ответить на
него можно только если второй список виден отдельно. Плюс общее: история
инцидентов, поведение под реальным потоком, поведение внешних систем в их
версиях, завязка потребителей на текущее поведение и вопрос «а нужна ли эта
функциональность вообще»;
- **каких документов проекта не хватило** — строкой на каждый, **с причиной**:
«`docs/security.md` в проекте нет», «есть, но периметр не назван». Строки
приходят из проходов; слить их в одну «документации не было» нельзя —
деградация поразрядная, и разные пробелы чинятся разным;
- **сработавшие потолки** — по строке на проход: сколько находок он показал,
каков был его потолок и что осталось за срезом. Проход обязан сообщить это сам;
не сообщил — так и напиши, это находка о прогоне.
**Четыре строки ты пишешь сам, на каждом прогоне, и ни один проход их не
принесёт.** Они про то, чего в конвейере нет вовсе, — а значит некому и
пожаловаться:
1. **Решения проекта не сверялись.** `docs/adr.*` — процессный документ, прогон
его не открывает. Расхождение изменения с записанным решением ловит сверка
документации — скилл `av-dev:doc-healthcheck`, а не ревью.
2. **Записанные наблюдения проекта не использовались.** `docs/research.*` — тоже
процессный. Всякое число в находках снято проходом на этом прогоне; числа без
приложенной команды замера в отчёте быть не должно.
3. **Поимённая сверка с руководствами по стилю языка не задавалась ни одним
проходом.** Различение «идиоматично против распространено» не спрашивает никто
с тех пор, как упразднён проход про идиоматичность.
4. **Альтернативной реализации, с которой можно сдиффить решения, у конвейера
нет.** Проход независимой реализации снят по стоимости, а не по замеру; «не
знаю, чего не знаю» больше не достаёт никто.
Плюс **пятая и шестая, обязательные на каждом прогоне цикла задачи**:
5. **Темы `security`, `operations` и `architecture` сверялись только с записанными
инвариантами `CLAUDE.md`**, дома этих тем не открывались. Свойства, которого
нет в инвариантах, не проверил никто. Разбор этих тем, построенный путь и
снятое число живут в скилле `av-dev:code-deep-review`.
6. **Форму решения не судил ни один проход.** Второй способ делать уже делаемое,
лишний слой, интерфейс ради мока — это тот же скилл; в цикле форму одобряет
человек на чекпоинте до кода.
Формулировка «критичных проблем не обнаружено» **запрещена** без этой секции: она
потребляет ощущение проверенности, ничего не гарантируя, и это хуже, чем
отсутствие отчёта — отсутствие человек хотя бы осознаёт.
## Чего этот проход принципиально не может поймать
Ничего нового ты не находишь по определению: ты не читаешь код в поисках
дефектов, ты работаешь с чужими выводами. Пропуск любого прохода — твой пропуск
тоже, и единственное, что ты можешь с этим сделать, — назвать его поимённо.
## Формат вывода
Строго секциями из контракта: `Блокирует мердж` (≤3) / `Стоит исправить сейчас`
(≤4) / `Гипотезы без доказательства` / `Урожай` / `Отложено в
av-dev:code-deep-review` / `Promote candidates` / `Границы покрытия`.
Перед секциями — сводка: режим прогона (`по change` или `без change`), состояние
гейта, **перечень тем с исходом по каждой**, сколько находок пришло на вход и
сколько осталось, сколько из них помечено `инлайн` и сколько `развилка`.
Последнее число — способ увидеть, во что обходится прогон человеку: развилок
больше двух на задачу значит, что либо задача не та, либо разметка действий
съехала.
## Ограничения
Писать можно только во временный каталог проекта (тесты для добычи оракулов). Код
не редактируй — это работа оркестратора.
@@ -1,9 +1,9 @@
---
name: task-form
description: "Проверка формы записи каталога задач по существу: форма заголовка по типу (цель — что приложение будет уметь, задача — что нужно сделать, идея — о чём она), «зачем», пересказывающее заголовок вместо состояния и боли, раздел «Затрагивает» с замыслом вместо границ, критерий приёмки с оракулом только на словах, предписание процесса в теле, и связь задачи со строкой «Завершения» её цели. Читает файл цели, на которую ссылается задача. Отдаёт готовые формулировки на замену и ничего не правит сам. Язык текста (залог, оценки, стоп-слова, англицизмы) смотрит отдельный агент doc-wording. Использовать после заведения или разбора пачки записей, до взятия в спринт и на переоценке. Только чтение."
description: "Проверка формы записи каталога задач по существу: тип, разошедшийся с содержанием записи, форма заголовка по типу (задача — что нужно сделать, разведка — о чём она), «зачем», пересказывающее заголовок вместо состояния и боли, раздел «Затрагивает» с замыслом вместо границ, критерий приёмки с оракулом только на словах, предписание процесса в теле. Отдаёт готовые формулировки на замену и ничего не правит сам. Язык текста (залог, оценки, стоп-слова, англицизмы) смотрит отдельный агент task-wording. Использовать после заведения или разбора пачки записей, при проверке готовности перед взятием в работу (tasks.py ready) и на груминге. Только чтение."
tools: Read, Grep, Glob
model: opus
color: yellow
model: sonnet
color: green
---
Ты — **проверка формы записи** каталога задач. Форма это не оформление: она
@@ -11,11 +11,11 @@ color: yellow
открывая код.
Оптика — смысл записи в её собственных рамках. Ты **не** судишь, нужна ли
задача, верно ли выбрана цель и не крупна ли она: это разбор, и его ведёт
человек со скиллом `tasks`.
задача и не крупна ли она: это разбор, и его ведёт человек со скиллом
`task-track`.
Границу с языком держи твёрдо. **Залог, оценки, стоп-слова, англицизмы, жаргон**
у агента `doc-wording`, и тебе они не поручены даже там, где бросаются в
у агента `task-wording`, и тебе они не поручены даже там, где бросаются в
глаза: две проверки одного места расходятся и начинают спорить. Увидел — скажи
одной строкой в конце доклада, не находкой. Исключение ровно одно: если
неудачное слово стоит **в заголовке** и мешает ему ответить на вопрос своего
@@ -27,9 +27,7 @@ color: yellow
## Что тебе дают
Список файлов записей (`docs/tasks/items/<slug>.md`) или каталог задач целиком.
Каталог тебе нужен и сам по себе: задача несёт тег `goal:<слаг>`, и **файл цели
ты открываешь**, иначе шестое правило не проверить.
Список файлов записей (`tasks/items/<slug>.md`) или каталог задач целиком.
Документы проекта — паспорт, архитектура, конвенции — если зовущий их назвал.
По ним видно, названа ли граница именем, которое в проекте существует.
@@ -38,29 +36,47 @@ color: yellow
1. **Заголовок отвечает на вопрос своего типа.**
Тип стоит первым полем меты — `- **Тип:** …`, — а в заголовке ему
соответствует эмодзи.
| Тип | Отвечает на | Форма |
| --- | --- | --- |
| `[goal]` | что приложение будет уметь | утверждение о возможности: «Соперником может быть компьютер» |
| задача | что нужно сделать | глагол в неопределённой форме, допускается «не» перед ним: «Печатать поле одним куском кода» |
| `[idea]` | о чём она | назывное, без обещания: «Подсказка следующего хода» |
| `feature`, 🐞 `fix`, 🧹 `chore` | что нужно сделать | глагол в неопределённой форме, допускается «не» перед ним: «Печатать поле одним куском кода» |
| 🔬 `research` | о чём разведка | назывное, без обещания: «Подсказка следующего хода» |
Описательный заголовок задачи («Лишние символы молча отбрасываются») называет
**состояние** и одинаково читается как жалоба и как задание. Заголовок цели в
форме действия («Сделать соперника-компьютер») превращает роадмап в список
работ — а он список возможностей.
**состояние** и одинаково читается как жалоба и как задание.
**Область работ — не цель.** «Работа со слиянием», «Рефакторинг вывода» не
отвечают ни на один из трёх вопросов; предложи возможность, которую эта работа
создаёт, и скажи, если из текста её не видно. **Свойство поведения —
законная возможность**: «исход слияния не зависит от порядка доставки» — цель,
а не абстракция.
**Область работ — не задача.** «Работа со слиянием», «Рефакторинг вывода» не
отвечают ни на один из двух вопросов; предложи формулировку, называющую, что
нужно сделать, и скажи, если из текста этого не видно.
2. **«Зачем» отвечает на «зачем нужна эта задача»** — состояние, остаток, боль,
2. **Тип сходится с тем, что в записи написано.** Тип — первое поле меты, и он
решает, каких разделов запись требует; разошедшийся тип врёт ровно там, где
по нему принимают решение. Проверяемые расхождения:
- **`fix`, у которого нечего воспроизвести**, — расхождение приняли на слово.
Либо это `research` («при каких условиях проявляется»), либо `feature`:
поведение никогда и не было заявлено, и чинить нечего;
- **`feature`, после которой снаружи ничего не меняется**, — это `chore`, и
сказать это честно дешевле, чем выдумывать пользовательскую пользу;
- **`chore`, меняющий наблюдаемое поведение**, — это `feature` или `fix`, и у
последнего другие требования (воспроизведение);
- **`research`, у которого «Вопрос» — это тема, а не вопрос.** «Разобраться с
выводом в терминалах» вопросом не является: на него нельзя ответить. Пока
вопроса нет, запись остаётся сырьём — и это законное состояние, но назови
его.
Раздел не из схемы своего типа (`Воспроизведение` у `chore`, критерии у
`research`) — сигнал того же расхождения, и `check` о нём говорит замечанием.
Твоя работа — сказать, **какой тип верен**, а не только что текущий не сходится.
3. **«Зачем» отвечает на «зачем нужна эта задача»** — состояние, остаток, боль,
— а не пересказывает заголовок. «Починить разбор хода» при заголовке «Не
отбрасывать молча лишние символы» — пересказ: читающий узнаёт то же самое
дважды и по-прежнему не знает, почему это лежит в беклоге.
3. **«Затрагивает» перечисляет границы, а не замысел.** Граница — то, у чего есть
4. **«Затрагивает» перечисляет границы, а не замысел.** Граница — то, у чего есть
внешняя сторона: команда и её аргументы, эндпоинт, таблица и миграция, формат
на диске, публичный тип пакета, внешний сервис. «Переписать хранилище на новый
драйвер» — замысел; проверяется вопросом «это можно назвать до того, как
@@ -72,53 +88,45 @@ color: yellow
становится двумя» вместо «выбор источника хода в модуле партии») — это уже
решение о том, как делать.
4. **У критерия назван оракул, и оракул проверяем.** «Оракул: глазами» на
5. **У критерия назван оракул, и оракул проверяем.** «Оракул: глазами» на
утверждение, которого глазами не проверить («компьютер не проигрывает ни в
одной партии»), — находка: слово стоит, проверки нет. Число критериев считает
`tasks.py check`, тебе оно неинтересно.
5. **Предписания процесса в теле нет.** «Делать профилем standard», «взять
6. **Предписания процесса в теле нет.** «Проверить вот таким проходом», «взять
такой-то агент» — это выбор, который делают, увидев изменение, а не при
постановке. Он же путь понизить требования решением, принятым до
проектирования.
6. **Задача называет, какую строку «Завершения» своей цели она двигает.**
Открой файл цели из тега `goal:<слаг>` и сверь. Три исхода, и все три —
разные находки:
- **строка не названа** — допиши предложение, какая это строка, если из текста
задачи видно; не видно — так и скажи;
- **строки с таким смыслом в «Завершении» нет** — либо задача не про эту цель,
либо у цели неполное «Завершение». Назови оба варианта, выбирать не тебе;
- **строка «Завершения», к которой не относится ни одна поданная задача**, —
это незакрытая часть возможности. Скажи о ней отдельно, вне списка находок
по файлам: это про набор, а не про запись.
У задачи **без цели** (`kind:fix`, `chore`, `research`) правило не
применяется вовсе — они служат работоспособности, а не направлению.
## Чего ты не проверяешь
Не своё бывает двух разных родов, и поступают с ними по-разному.
**Чужому подрядчику — строкой в границах покрытия.** Язык у `doc-wording`;
увидел — назови в конце одной строкой, чтобы находка не пропала, но находкой не
оформляй.
**Чужому подрядчику — строкой в границах покрытия.** Язык у `task-wording`;
согласованность документов канона между собой у `doc-consistency`, их
соответствие коду у `doc-code-drift` — до задач эти двое не доходят вовсе.
Увидел не своё — назови в конце одной строкой, чтобы находка не пропала, но
находкой не оформляй.
**Машинной проверке — вообще ничего.** Всё, что ловит `tasks.py check` (наличие
разделов, число критериев, состав и написание секций, теги, тег `question` при
непустом разделе «Вопросы», согласованность индексов, битые ссылки, форма
заголовка как строки), **не пиши даже строкой**: это не потерянная находка, а
уже проверенное. Повторять машинную проверку словами — заводить второй дом для
одного правила.
**Машинной проверке — вообще ничего.** Всё, что ловит `tasks.py check` (состав и
написание секций, теги, тег `question` при непустом разделе «Вопросы»,
согласованность индекса, битые ссылки, форма заголовка как строки), **не пиши
даже строкой**: это не потерянная находка, а уже проверенное. Повторять машинную
проверку словами — заводить второй дом для одного правила.
**Содержание работы**: нужна ли задача, верно ли выбрана цель, не крупна ли она,
достаточна ли декомпозиция. Шестое правило подходит к этому близко и
останавливается там, где кончается сверка с текстом цели. Об этом молчи.
**Наличие разделов и число критериев `check` поимённо не называет** — он считает
их строкой здоровья, а поимённо судит `tasks.py ready` на входе в работу.
Отсутствующий раздел сам по себе всё равно не твоя находка (её увидит `ready`);
твоя — раздел, который **есть и лжёт**: границы вместо замысла, критерий с
оракулом только на словах.
**Содержание работы**: нужна ли задача, не крупна ли она, достаточна ли
декомпозиция. **И место в списке**: порядок строк значит зависимость на стройке и
важность на доработке, а ты записи видишь поштучно, вне списка. Об этом молчи.
## Порог вмешательства
<!-- копия: порог-правки из av-dev-pm/skills/canon/references/language.md -->
<!-- копия: порог-правки из av-dev/shared/language.md -->
**Правка без нарушенного правила не делается.** Текст, переписанный «чтобы
звучало лучше», обесценивает список замечаний: когда половина из них вкусовая,
@@ -139,9 +147,9 @@ color: yellow
## Доклад
Находки по одной, в порядке важности: заголовок → «зачем» → границы → критерии
связь с целью. Порядок такой, потому что заголовок и «зачем» — это всё, что
видно в индексе, а по индексу и выбирают.
Находки по одной, в порядке важности: заголовок → «зачем» → границы → критерии.
Порядок такой, потому что заголовок и «зачем» — это всё, что видно в индексе, а
по индексу и выбирают.
```
<файл>
@@ -151,11 +159,8 @@ color: yellow
почему: <одна фраза>
```
Отдельным блоком после находок — **строки «Завершения» без задач**, если такие
нашлись: цель, строка, и что это значит.
В конце — **границы покрытия**: сколько записей просмотрено из скольких, какие
цели открыты, какие не смотрел и почему. Отчёт без этой строки читается как
не смотрел и почему. Отчёт без этой строки читается как
«беклог проверен», не сообщая, какая его часть осталась нетронутой. Туда же —
строка «замечено не по моей части», если бросился в глаза язык; машинно
проверяемое в неё **не идёт**.
+311
View File
@@ -0,0 +1,311 @@
---
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` и `REJECTED.md`, где та же запись представлена строкой. **Строка индекса вычитывается наравне с файлом**: по ней запись
выбирают, не открывая тела, и «зачем» в ней повторяется дословно.
Плюс, если зовущий их назвал, документы проекта — паспорт, архитектура,
конвенции: по ним проверяется, известен ли термин. **Не назвали — считай
известными только те слова, что встречаются в других поданных записях**, и
говори об этом в границах покрытия.
## Правила
Дом — `shared/language.md` в репозитории плагина, и там же объяснено, зачем
стиль вообще нужен. Здесь только то, что нужно тебе для работы.
<!-- копия: язык-правила из av-dev/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`. Транслит
нечитаем тому, кто ищет по смыслу, и не сокращается, а имя стоит в ссылках,
коммитах и путях, которые набирают руками. Переименование — **перенос ссылок
одним проходом**, а не правка одного файла.
10. **Счёт корпуса не пишется словами.** «Пять ревью», «три capability»,
«десять проходов», «четыре документа канона» — это факт о корпусе, а дом у
такого факта сам корпус. Переписанный в прозу, он расходится с ним на первом
же пополнении, и расходится **молча**: фраза остаётся грамматически исправной
и правдоподобной, а проверить её можно только пересчётом, которого никто не
делает.
Сослаться можно двумя способами, и ни один не стареет:
| Как | Пример |
| --- | --- |
| на конкретную запись — именем, слагом, датой | «ревью от 3 августа», `adr/0007-queue-as-table.md`, capability `recognition` |
| на корпус целиком | «ревью проекта», «capability, объявленные в `openspec/specs/`» |
Величина нужна читателю редко, а когда нужна — её называет сам корпус в
момент чтения: каталог, индекс, команда. Абзац её только запоминает.
**Замер с названным происхождением — не счёт корпуса.** «Прозаический триггер дал 6
записей ADR на 43 изменения» описывает прошлое, а прошлое не пополняется: это
факт по правилу 2, и трогать его нельзя. Разница проверяется вопросом
«изменится ли число само, без правки текста».
**Число, стоящее заголовком к перечню, приведённому тут же, правилом не
задето.** «Три исхода:» с тремя пунктами под ним — не ссылка на корпус, и
расходится оно не втихую, а вместе со списком, который правят в той же
строке. Уехал перечень в другой файл — число уезжает с ним, а на его месте
остаётся ссылка.
<!-- /копия: язык-правила -->
### Что из этих правил докладывается особым образом
**Правило 4, поля меты.** «Зачем» по формату — одно предложение, потому что
повторяется строкой индекса. Предложить разбить его надвое — находка **против**
формата, а не по нему; тесно — предлагай сокращение.
**Правило 8, неизвестный термин.** Своей догадки не подставляй — ты не знаешь
предметную область. Пиши «термин «X» не встречается ни в документах, ни в других
поданных записях — введи строкой или назови известным словом». Свой словарь у
отдельной задачи — самый дешёвый способ сделать беклог нечитаемым тому, кто
вернётся к нему через квартал.
**Правило 9, имя файла.** Кириллицу в слаге и не-kebab-case ловит `tasks.py check`
про них молчи. Твоё — **транслит**, потому что машина проверяет его эвристикой и
ловит не всё: `sostoyanie-partii` проходит мимо неё. Находка — готовый
английский слаг на замену плюс напоминание, что переименование это перенос
ссылок одним проходом, а не правка одного файла.
**Правило 10, счёт корпуса.** Пересчитывать корпус не надо: находка — в самом
числе, а не в том, что оно разошлось. Число, верное сегодня, — та же находка. В
записях счёт заводится в «Затрагивает» («три эндпоинта», «четыре миграции») и в
критериях приёмки, и там он опаснее прочего: критерий, сверяемый по числу,
пройдёт на другом составе работ. Предложение — готовая замена: перечислить
поимённо или назвать корпус целиком. Перечень, приведённый тут же под числом, не
трогай.
## Чего ты не проверяешь
Не своё бывает двух разных родов, и поступают с ними по-разному.
**Чужому подрядчику — строкой в границах покрытия.** Форма записи у `task-form`;
язык документов проекта у `doc-wording`; их согласованность между собой у
`doc-consistency`, соответствие коду у `doc-code-drift` — до записей эти двое не
доходят вовсе, но если ты открыл документ как словарь и увидел расхождение в нём
самом, оно их. Увидел — назови в конце одной строкой, чтобы находка не пропала,
но находкой не оформляй.
**Машинной проверке — вообще ничего.** Всё, что ловит `tasks.py check` (состав и
написание секций, теги, тег `question` при непустом разделе «Вопросы»,
согласованность файлов с индексами, битые ссылки), **не пиши даже строкой**: это
не потерянная находка, а уже проверенное. Повторять машинную проверку словами —
заводить второй дом для одного правила. Наличие разделов своего типа и число
критериев `check` только считает — поимённо их судит `tasks.py ready`, и это
тоже не твоя находка: твоя — язык того, что уже написано.
**Содержание работы**: нужна ли задача, не крупна ли она, достаточна ли
декомпозиция. Это разбор, и его ведёт человек со скиллом `task-track`.
**Полезное действие, параллельность и работающий заголовок** — тоже не твои.
Они в доктрине языка, судит их человек: находка по ним требует увидеть текст
целиком, а не фразу.
## Порог вмешательства
<!-- копия: порог-правки из av-dev/shared/language.md -->
**Правка без нарушенного правила не делается.** Текст, переписанный «чтобы
звучало лучше», обесценивает список замечаний: когда половина из них вкусовая,
перестают читать весь список, и вместе с ним пропадают настоящие находки.
Сомневаешься — не правь. Формулировка, которая просто **не твоя**, — не находка.
**Систематичность нарушения — не довод в его пользу.** Одна и та же ошибка в
пяти файлах не становится «принятым стилем»: чаще это значит, что правило не
применялось вовсе, — и находка тем важнее. «Так сделано везде» годится как
основание для **одной находки на весь набор** («правило N нарушено в пяти
записях, перечень: …»), но не как основание промолчать. Принятым считается
только то, что назвал зовущий или что записано в конвенциях проекта.
<!-- /копия: порог-правки -->
Одна запись может дать несколько находок, но каждое место правится один раз: не
предлагай два варианта на выбор, предлагай лучший.
## Доклад
<!-- копия: вычитка-доклад из av-dev/shared/language.md -->
Находки по одной, в порядке важности: залог и оценки → жаргон и англицизмы →
стоп-слова. Первые меняют, **что** читатель понимает; последние — только сколько
он на это тратит.
```
<файл>
правило: <номер и короткое имя>
сейчас: <как написано>
предложение: <готовая формулировка, подставляемая как есть>
почему: <одна фраза>
```
В конце — **границы покрытия**: сколько файлов просмотрено из скольких, какие не
смотрел и почему, и по чему проверялись термины (документы проекта названы или
нет). Отчёт без этой строки читается как «всё вычитано», не сообщая, какая часть
осталась нетронутой. Туда же — строка «замечено не по моей части»; машинно
проверяемое в неё **не идёт**.
Ничего не нашёл — так и скажи одной строкой. Пустой доклад с границами покрытия
полезнее выдуманной находки.
<!-- /копия: вычитка-доклад -->
**Находку в заголовке или в «зачем» отмечай особо.** По ним запись выбирают, и
подставляются они командой, а не редактором: зовущий обязан показать
предложенное человеку вместе с тем, что было. Прочие правки в теле применяются
сразу.
+59
View File
@@ -0,0 +1,59 @@
# Чего может не быть
**Это дом.** Правило нужно почти каждому скиллу: любой приходит в проект, где
может не оказаться ни документов канона, ни каталога задач, ни `openspec/`, а
рядом может не стоять внешний плагин, которого он ждёт. Ни один скилл правилом
не владеет, поэтому дом стоит в `shared/`, а скиллы везут **копии**, помеченные
разметкой `copies.py`.
Правится **здесь**. Копия, поправленная у себя, — расхождение, а не правка.
До слияния правило называлось «граница плагинов» и говорило о соседе:
`av-dev-docs`, `av-dev-tasks` и `av-dev-code` ставились порознь, и каждый обязан
был пережить отсутствие двоих. Плагин теперь один, а правило осталось, и не по
инерции: **отсутствовала всё это время не установка, а раскладка проекта**, и
узнавалась она следом на диске, а не перечнем плагинов. Перечень того, чего
может не быть, стал короче на три имени — механика не изменилась вовсе.
Правило завели по подсчёту: к первому расколу оно стояло в пяти местах в пяти
редакциях, и три из пяти молчали о том, ради чего написано, — что делать, когда
недостающее нашлось.
<!-- дом: отсутствие -->
**Скилл не вправе считать раскладку проекта полной.** Части заводятся порознь и
живут порознь; каждая узнаётся своим следом:
| Чего нет | Как видно | Чего теперь не делает никто |
| --- | --- | --- |
| настройки av-dev | нет `.av-dev.toml` в корне | проект под процесс не заводился; версии нет, настроек нет |
| документы канона | нет `docs/` | проектную конкретику брать неоткуда — темы, инварианты, прецеденты |
| учёт работ | нет каталога задач | запись остаётся владельцу: назови её текстом в докладе |
| источник требований | нет `openspec/config.yaml` | цикл SDD не запускается: спеки не с чем сверять |
**Свой скилл зовётся полным именем** — `av-dev:canon`, `av-dev:task-track`,
`av-dev:code-review`. Короткое имя может разрешиться в устаревшую проектную
копию из `.claude/skills/`, и подмены не будет видно ни в докладе, ни в
поведении.
**Внешний плагин может не стоять.** Их два: `opsx:*` — цикл SDD, и
`av-dev-git:commit` — сообщения коммитов. Путь в дерево чужого плагина не
пишется никогда: `$CLAUDE_PLUGIN_ROOT` ведёт только в своё дерево, а
вычисленный от него путь к соседу либо не откроется, либо откроет чужую
установку. Нужен чужой справочник — зови владеющий им скилл, он прочитает его
сам.
**Отсутствие — исход, а не поломка.** Назови строкой доклада, чего теперь не
делает никто, и продолжай работу. Молчать нельзя: пропуск неотличим от
сделанного. Выдумывать обходной путь нельзя тоже.
**Присутствие узнаётся следом в проекте, а не объявлением.** Перечня того, что
здесь заведено, проект не ведёт — он разошёлся бы с действительностью молча.
<!-- /дом: отсутствие -->
**Что в дом не идёт: чем именно оборачивается нехватка у тебя.** «Нет каталога
задач — учёт остаётся владельцу» знает конвейер; «нет `openspec/``docs.py`
о каталоге молчит» знает канон. Правило общее, последствие местное, и держать
последствия здесь значило бы завести дом, который знает про всех своих
потребителей.
+150
View File
@@ -0,0 +1,150 @@
# Оси процесса
**Это дом перечня, а не значений.** Что означает каждое значение и как оно
работает, знает владелец оси — здесь только сама ось, её дом и **чего она не
решает**. Второй пересказ механики разошёлся бы с первым; перечень же нужен
целиком и в одном месте, потому что вопрос «а не задаёт ли это глубину ревью»
задают из скилла, который ревью не ведёт.
**Одну ось перечень уже терял, и терял молча.** Метка задачи — `small`, `medium`,
`large` — правила состав ревью кода, пока состав не стал постоянным; ось снята
вместе с проходом, который её считал. Строка в журнале решений есть, а здесь от
неё не осталось ничего — так и должно быть: перечень описывает то, что ветвится
сегодня.
**Трёх осей он не досчитывал и в обратную сторону.** Глубина темы, разметка
действия и род правки документа ветвили поведение годами, а в перечне их не было:
каждая живёт в своём скилле, и оттуда её видно, а отсюда — нет. Ровно за этим
перечень и заведён: вопрос «а не задаёт ли это глубину ревью» задают из скилла,
который ревью не ведёт.
**Ось — это закрытый перечень значений, по которому что-то ветвится.** Признак
проверяемый, и он отсекает похожее: темы ревью и документы проекта — списки
**открытые**, их пополняет проект, и перечень в плагине протух бы на первом же
своём документе. Модель прохода — не ось, а цена прогона; её дом — «Модель по
проходу» в `code-review`, механизация — `frontmatter.py`.
## Перечень
| Ось | Значения | Дом |
| --- | --- | --- |
| стадия проекта | `build` `support` | `task-track/SKILL.md`, «Две стадии» |
| тип записи | `feature` `fix` `chore` `research` | `task-track/SKILL.md`, «Тип записи» |
| форма постановки | запись каталога · текст | `code-resolve/SKILL.md`, «Вход» |
| сценарий | решение · обслуживание · разведка | `code-resolve/SKILL.md`, «Развилка» |
| режим прогона | по change · без change | здесь, ниже |
| род правки документа | отражение · новое | `doc-sync/SKILL.md`, «Два рода правок» |
| глубина темы | сверка · разбор · доказательство | `code-review/SKILL.md`, таблица тем |
| разметка действия | инлайн · развилка | `code-review/SKILL.md`, «Что происходит с находками» |
| категория документа | тема · источник темы · процессный | `canon/references/canon.md` |
| severity находки | `critical` `major` `minor` `nit` | `code-review/references/finding-contract.md` |
| коды выхода | 0 1 2 3 4 | здесь, ниже |
Две оси стоят домом **здесь**, и обе по одной причине: владельца у них нет.
Коды выхода делят все скрипты плагина и зовущие их скиллы, режим прогона —
конвейер, сценарий обслуживания и уставы вычитки.
## Что на что влияет
Клетка называет **место**, где связка описана; сама связка живёт там.
| Влияет | На что | Где описано |
| --- | --- | --- |
| стадия проекта | что значит порядок строк беклога: зависимость или важность | `task-track/SKILL.md`, «Две стадии» |
| стадия проекта | сколько у беклога секций, как его пополняют, применим ли груминг | там же и `task-groom/SKILL.md`, «Груминг — операция доработки» |
| стадия проекта | глубину ревью — **не влияет, и это записано явно** | `task-track/SKILL.md`, «Тип записи» |
| стадия проекта | тип записи — **не влияет, и это записано явно** | `task-track/SKILL.md`, «Две стадии» |
| стадия проекта | как серьёзность находки ложится в список | `task-track/references/from-review.md` |
| стадия проекта | где сценарии кладут свой исход и чем закрывают переходное состояние | `code-resolve/references/research.md`, `task-track/references/adopt.md` |
| форма постановки | проверку готовности, кто называет тип, есть ли шаг закрытия | `code-resolve/SKILL.md`, «Постановка текстом» |
| форма постановки | сценарий и глубину ревью — **не влияет, и это записано явно** | там же: развилка у обеих форм общая |
| тип записи | сценарий — **предлагает**, подтверждает предмет работы | `task-track/SKILL.md`, «Тип записи» |
| тип записи | глубину ревью — **не влияет, и это записано явно** | там же |
| сценарий | режим прогона: обслуживание идёт без change | `code-resolve/references/maintain.md` |
| режим прогона | состав проходов и саму возможность запуска прохода | `code-review/SKILL.md`, «Прогон без change» |
| категория документа | заводит ли документ направление проверки | `canon.md`, «Три категории» |
| severity | что с находкой делают дальше | `code-review/SKILL.md`, «Что происходит с находками» |
| род правки | спрашивают ли человека перед письмом в документ | `doc-sync/SKILL.md`, «Два рода правок» |
| глубина темы | что проход делает с домом темы и какой потолок у находок | `code-review/SKILL.md`, таблица тем |
| разметка действия | чинится находка молча или уходит человеку вопросом | `code-review/SKILL.md`, «Что происходит с находками» |
| разметка действия | возвращается ли прогон на чекпоинт — **не задаёт**: возврат старше развилки и решается признаком «меняются ли дельта-спеки» | `code-resolve/references/solve.md`, шаг 5 |
| сценарий | какова доля отражения в синке: обслуживание двигает факты и потому спрашивает редко | `code-resolve/references/maintain.md`, шаг 5 |
**Четыре клетки пусты, и это сказано намеренно, а не забыто.**
**Категория документа × режим прогона.** На прогоне **по change** своя тема
проекта закрыта: `review-basics` — её приёмник, и запускается он тогда и только
тогда, когда такие темы у проекта есть. На прогоне **без change** план фиксирован
сценарием — `autotests`, `operations`, `conventions`, — и своих тем проекта в нём
нет. Значит, документ, заведённый проектом как тема, на обслуживании не смотрит
никто, и строкой это нигде не называется.
**Род правки × severity и × режим прогона.** Не влияет ни туда, ни обратно: род
правки — свойство того, что пишется в документ, и с находкой ревью он не
встречается. Находка, доехавшая до конвенции, меняет род не сама по себе, а тем,
что становится новой нормой, — и спрашивается тогда как всякое новое.
**Стадия проекта × режим прогона.** Не влияет: режим выбирает сценарий. Прогон
обслуживания на стройке — обычное дело (первые шаги плана заводят гейт и сборку),
и идёт он там так же, как на доработке.
**Стадия проекта × категория документа, × коды выхода и × форма постановки.** Не
влияет: категория — свойство документа, коды — общий словарь скриптов, а форму
постановки выбирает тот, кто зовёт скилл, и на стройке она такая же, как на
доработке. Названо потому, что перечень объявлен полным, и клетка без ответа
читается как забытая.
**Режим прогона × severity.** Триаж обязателен всегда, в том числе без change. Но
часть оснований `critical` — построенный путь к отказу, замер — добывается только
скиллом `av-dev:code-deep-review`, а в цикле задачи не добывается ни на одном
прогоне. Значит ли это, что `critical` там не бывает вовсе, или что его основания
другие, не сказано.
## Режим прогона
<!-- дом: режим-прогона -->
**Прогон ревью идёт в одном из двух режимов, и режим — не глубина.**
- **По change** — обычный прогон цикла задачи: есть дифф и дельта-спеки, состав
постоянный и живёт в конвейере.
- **Без change** — дельта-спек нет по построению, и вместе с ними нет темы
`requirements`. План фиксирован и назван вызывающим; так идёт сценарий
обслуживания.
**Режим правит состав, а не глубину.** Глубина темы стоит в таблице тем
конвейера, одна на все прогоны по change; на прогоне без change её называет план
сценария — иначе проход, чьей темы в плане нет, взял бы глубину наугад.
**Третьего режима у конвейера нет.** Скилл `av-dev:code-deep-review` конвейер не
зовёт вовсе: состав, глубина и вход у него свои, а общее с конвейером — уставы
проходов и контракт находок.
<!-- /дом: режим-прогона -->
## Коды выхода
<!-- дом: коды-выхода -->
**Коды выхода — общий словарь всех скриптов `av-dev`. Ветвись на коде, а не на
тексте вывода.**
| Код | Что случилось |
| --- | --- |
| 0 | сошлось |
| 1 | дрейф: рабочая ситуация, чинится |
| 2 | ошибка употребления: аргументы или нарушенное правило |
| 3 | окружение: не тот каталог, битый конфиг, нет инструмента |
| 4 | внутренний сбой — дефект скрипта, доложить |
**Различать 1 и 3 обязательно.** «Дрейф» — рабочая ситуация, и чинится она
правкой предмета; «окружение» — нерабочая, и повтор той же командой не поможет.
Одинаковая реакция на них неверна в обоих случаях.
<!-- /дом: коды-выхода -->
Словарь был объявлен «общим» в одиннадцати местах, и каждое объявление
перечисляло **свой** набор соседей: «тот же, что у `tasks.py`», «тот же, что у
`tasks.py`, `docs.py` и `copies.py`», «общий словарь скриптов av-dev». Ни одно из
них не было домом, все — списки по памяти. Отсюда дом здесь: у словаря восемь
скриптов-потребителей и ни одного владельца.
+355
View File
@@ -0,0 +1,355 @@
#!/usr/bin/env python3
"""Служебный файл проекта `.av-dev.toml`: чтение, запись, узнавание прежних.
**Это дом.** Файл один на весь плагин, поэтому и разбор у него один: `docs.py`
и `tasks.py` берут настройки отсюда, а не каждый своим кодом. Два разбора одного
формата это два дома для одной схемы, и расходятся они молча: первым
разъезжается не значение ключа, а то, что скрипт делает, ключа не увидев.
Формат TOML выбран ради **комментариев**: файл лежит в чужом репозитории, и
человек, открывший его через полгода, обязан прочитать в нём, что означает
число. JSON комментариев не знает, и объяснение приходилось держать в
документации, то есть в другом файле.
Читается `tomllib` из стандартной библиотеки (python 3.11+), пишется руками:
писателя TOML в стандартной библиотеке нет, а комментарии переживают только
построчную правку. Поэтому версия двигается заменой одной строки, а не
перезаписью файла иначе повышение канона стирало бы то, ради чего формат и
взят.
Схема:
version = 1 # версия раскладки av-dev, целое число
[docs]
migrations = "путь/к/миграциям" # необязателен: есть БД — есть ключ
[tasks]
dir = "tasks" # каталог задач от корня репозитория
stage = "build" # стадия проекта: build | support
items = "items" # имена частей каталога — необязательны
backlog = "BACKLOG.md"
Коды выхода зовущих скриптов общий словарь av-dev; отсюда возвращается
исключение `ConfigError`, а решает по нему вызывающий.
"""
from __future__ import annotations
import re
import tomllib
from pathlib import Path
# Имя файла называет владельца: раскладку ведёт плагин `av-dev`. До слияния
# плагинов файлов было два — `docs/.docs.json` (версия канона) и
# `<каталог задач>/.tasks.json` (версия формата задач), и версии двигались
# порознь, потому что плагины ставились порознь. Плагин теперь один, версия
# одна, и дом у неё в корне репозитория: настройки нужны и проекту без `docs/`,
# и проекту без каталога задач, а корень есть у обоих.
CONFIG_NAME = ".av-dev.toml"
# Прежние дома. Читаются не для работы, а для узнавания: увидели — говорим
# «старая раскладка, нужен upgrade», и это одна строка вместо отказа, за которым
# человек идёт заводить второй файл рядом с первым.
LEGACY = ("docs/.docs.json", "docs/.pm.json")
LEGACY_TASKS = ".tasks.json"
# Версия раскладки — одна на плагин. Журнал версий — references/changelog.md
# скилла `canon`, повышает его операция `upgrade`.
VERSION = 5
VERSION_KEY = "version"
class ConfigError(Exception):
"""Файл есть, но прочитать его нельзя: битый TOML или не та схема."""
def find_root(start: Path | None = None) -> Path | None:
"""Корень проекта: где лежит `.av-dev.toml`, иначе где лежит `.git`.
Обе опоры нужны: до `adopt` файла ещё нет, а работать по каталогу задач уже
можно. Возвращается None, когда нет ни того, ни другого, тогда зовущий сам
решает, отказ это или неприменимость.
**Подъём останавливается на первом `.git`, и это не деталь.** Репозиторий
внутри репозитория обычное дело, и без границы конфиг соседа выигрывал бы
у собственного: вложенный проект объявлялся бы здоровым по чужому файлу, а
запись настроек уходила бы в чужой репозиторий. Свой файл ищется **до**
границы включительно, чужой не ищется вовсе.
"""
here = (start or Path.cwd()).resolve()
for base in (here, *here.parents):
if (base / CONFIG_NAME).is_file():
return base
if (base / ".git").exists():
return base # корень репозитория есть, настроек в нём нет
return None
def read(root: Path) -> dict:
"""Настройки проекта. Файла нет — пустой словарь, это не ошибка."""
path = root / CONFIG_NAME
if not path.is_file():
return {}
try:
# Читаем байтами: `tomllib.load` сам знает про кодировку TOML, а
# `read_text` на файле не в UTF-8 роняет UnicodeDecodeError — ошибку
# окружения, которая ушла бы наружу внутренним сбоем.
with path.open("rb") as fh:
data = tomllib.load(fh)
except tomllib.TOMLDecodeError as exc:
raise ConfigError(f"{CONFIG_NAME} не разбирается как TOML: {exc}") from exc
except (OSError, ValueError) as exc:
raise ConfigError(f"{CONFIG_NAME} не читается: {exc}") from exc
_validate(data)
return data
# Ключи верхнего уровня. Секции знают свои ключи сами: `[docs]` проверяет
# `docs.py`, `[tasks]` — `tasks.py`. Здесь только то, что образует сам файл.
TOP_KEYS = (VERSION_KEY, "docs", "tasks")
def check_keys(data: dict, known: tuple[str, ...], where: str) -> None:
"""Неизвестный ключ — отказ, а не безмолвный пропуск.
Ключ, положенный не туда (`migrations` верхним уровнем вместо `[docs]`
ровно так он лежал в прежнем `.docs.json`, и ровно так его перенесут руками),
иначе не значит ничего: проверка объявляет себя неприменимой, отчёт выходит
зелёным, и на месте настройки оказывается тишина.
"""
unknown = sorted(set(data) - set(known))
if unknown:
raise ConfigError(
f"{CONFIG_NAME}: неизвестные ключи {where}: {', '.join(unknown)}"
f" (известны: {', '.join(known)})"
)
def _validate(data: dict) -> None:
got = data.get(VERSION_KEY)
if VERSION_KEY in data and (isinstance(got, bool) or not isinstance(got, int)):
raise ConfigError(
f"{CONFIG_NAME}: ключ «{VERSION_KEY}» — версия раскладки,"
f" ожидалось целое число, а не {got!r}"
)
for name in ("docs", "tasks"):
got_section = data.get(name)
if got_section is not None and not isinstance(got_section, dict):
raise ConfigError(
f"{CONFIG_NAME}: секция [{name}] — ожидалась таблица настроек,"
f" а не {got_section!r}"
)
check_keys(data, TOP_KEYS, "верхнего уровня")
def section(cfg: dict, name: str) -> dict:
got = cfg.get(name, {})
return got if isinstance(got, dict) else {}
def version(cfg: dict) -> int | None:
got = cfg.get(VERSION_KEY)
return got if isinstance(got, int) and not isinstance(got, bool) else None
def legacy_files(root: Path, tasks_dir: Path | None = None) -> list[str]:
"""Следы прежней раскладки — то, что говорит «проект жил до слияния».
Каталог задач передаётся отдельно: до чтения настроек его путь неизвестен, а
искать `.tasks.json` по всему дереву значит гадать.
"""
root = root.resolve()
found = [rel for rel in LEGACY if (root / rel).is_file()]
for base in filter(None, (tasks_dir, root / "tasks", root / "docs" / "tasks")):
# Каталог задач приходит и относительным — таким его печатают в
# сообщениях; для сравнения с корнем он обязан быть абсолютным.
path = (base if base.is_absolute() else Path.cwd() / base) / LEGACY_TASKS
if not path.is_file():
continue
path = path.resolve()
rel = path.relative_to(root).as_posix() if path.is_relative_to(root) else str(path)
if rel not in found:
found.append(rel)
return found
def quote(value: str) -> str:
"""Значение как строка TOML: экранирование, а не конкатенация в кавычки.
Без него имя файла с кавычкой или путь с обратной косой чертой ломают
**весь** файл: `tomllib` отказывается разбирать его целиком, и оба скрипта
после этого отвечают кодом 3 на любую команду. Пишет сюда машина, а
последствия достаются человеку, который такого имени не выбирал.
"""
out = value.replace("\\", "\\\\").replace('"', '\\"')
out = out.replace("\n", "\\n").replace("\r", "\\r").replace("\t", "\\t")
return f'"{out}"'
def _strip_comment(line: str) -> str:
"""Строка без хвостового комментария. Кавычки уважаются: `#` внутри них — текст."""
quoted = False
for i, ch in enumerate(line):
if ch == '"' and (i == 0 or line[i - 1] != "\\"):
quoted = not quoted
elif ch == "#" and not quoted:
return line[:i]
return line
def _is_header(line: str, name: str | None = None) -> bool:
"""Заголовок секции — по разбору, а не по совпадению строки.
`[tasks] # имена частей` — законный TOML и ровно та возможность, ради
которой формат и взят. Сравнение строк её не узнаёт, дописывает вторую
таблицу с тем же именем, и `tomllib` отвергает файл целиком.
"""
body = _strip_comment(line).strip()
if not (body.startswith("[") and body.endswith("]")):
return False
return name is None or body[1:-1].strip() == name
def set_version(root: Path, number: int) -> None:
"""Двинуть версию, не тронув остального: правится одна строка.
Перезапись файла целиком стёрла бы комментарии то единственное, ради чего
формат и выбран.
**Ищется только ключ верхнего уровня** то есть выше первого заголовка
секции. `version` внутри `[docs]` принадлежит проекту и значит что угодно
своё; двинув его, мы объявили бы приведённым не то, о чём речь, и оставили
бы настоящую версию неназванной. Ключа нет вовсе строка встаёт первой, до
всякой секции, по той же причине.
"""
path = root / CONFIG_NAME
lines = path.read_text(encoding="utf-8").splitlines() if path.is_file() else []
end = next((i for i, ln in enumerate(lines) if _is_header(ln)), len(lines))
# Значение берётся до комментария и может быть каким угодно — в том числе
# строкой в кавычках: файл правят руками. Заменяется оно целиком, иначе
# рядом появился бы второй ключ `version`, и файл перестал бы разбираться.
pattern = re.compile(rf"^(\s*{VERSION_KEY}\s*=\s*)([^#]*?)(\s*(?:#.*)?)$")
for i in range(end):
match = pattern.match(lines[i])
if match:
lines[i] = f"{match.group(1)}{number}{match.group(3)}"
break
else:
lines.insert(0, f"{VERSION_KEY} = {number}")
path.write_text("\n".join(lines) + "\n", encoding="utf-8")
def merge_section(root: Path, name: str, values: dict) -> list[str]:
"""Дописать ключи в секцию, не тронув остального. Возвращает дописанное.
Правка построчная по той же причине, что и у версии: перезапись файла
целиком стёрла бы комментарии. Ключ, который в секции уже есть, не трогается
вовсе файл в чужом репозитории правит человек, и затирать его значение
своим умолчанием нельзя. **Что дописано, а что нет, решает зовущий:** список
возвращается, и молчать о неписаном ему нельзя.
"""
path = root / CONFIG_NAME
lines = path.read_text(encoding="utf-8").splitlines() if path.is_file() else []
start = next((i for i, ln in enumerate(lines) if _is_header(ln, name)), None)
if start is None:
if not values:
return []
block = ([""] if lines and lines[-1].strip() else []) + [f"[{name}]"]
block += [f"{k} = {quote(v)}" for k, v in values.items()]
path.write_text("\n".join([*lines, *block]) + "\n", encoding="utf-8")
return list(values)
end = next((i for i in range(start + 1, len(lines)) if _is_header(lines[i])),
len(lines))
body = lines[start + 1:end]
have = {ln.split("=", 1)[0].strip() for ln in map(_strip_comment, body)
if "=" in ln}
added = [k for k in values if k not in have]
if not added:
return []
# Пустые строки в хвосте секции — отбивка перед следующим заголовком.
# Дописываем до неё, а её возвращаем на место: иначе файл слипается.
trailing = 0
while body and not body[-1].strip():
body.pop()
trailing += 1
insert = [f"{k} = {quote(values[k])}" for k in added]
lines[start + 1:end] = [*body, *insert, *([""] * trailing)]
path.write_text("\n".join(lines) + "\n", encoding="utf-8")
return added
def set_section_key(root: Path, name: str, key: str, value: str) -> None:
"""Заменить значение ключа секции, не тронув остального.
Отличается от `merge_section` ровно тем, ради чего и заведена: та **не
трогает** ключ, который уже есть, потому что дописывает умолчания в чужой
файл. Здесь же значение меняет команда, которую позвал человек, и не
переписать его значило бы промолчать о выполненном действии. Ключа нет
он дописывается, секции нет заводится: и то и другое законное состояние
файла, который правят руками.
"""
path = root / CONFIG_NAME
lines = path.read_text(encoding="utf-8").splitlines() if path.is_file() else []
start = next((i for i, ln in enumerate(lines) if _is_header(ln, name)), None)
if start is None:
merge_section(root, name, {key: value})
return
end = next((i for i in range(start + 1, len(lines)) if _is_header(lines[i])),
len(lines))
pattern = re.compile(rf"^(\s*{re.escape(key)}\s*=\s*)([^#]*?)(\s*(?:#.*)?)$")
for i in range(start + 1, end):
if (match := pattern.match(lines[i])):
lines[i] = f"{match.group(1)}{quote(value)}{match.group(3)}"
path.write_text("\n".join(lines) + "\n", encoding="utf-8")
return
merge_section(root, name, {key: value})
def missing_keys(root: Path, name: str, values: dict) -> dict:
"""Ключи секции, которые уже есть и разошлись с тем, что мы собирались дать.
`merge_section` чужого значения не трогает и правильно делает, но
промолчать о расхождении нельзя: `dir` из настроек и `--dir` из вызова,
разойдясь, оставляют каталог, до которого потом не дотянется никто.
"""
have = section(read(root), name)
return {k: have[k] for k, v in values.items() if k in have and have[k] != v}
def skeleton(number: int, docs: dict | None = None, tasks: dict | None = None) -> str:
"""Свежий файл с комментариями — тем, ради чего взят TOML.
Пустая секция пишется всё равно: строка «ключа нет, потому что БД нет»
читается как решение, а её отсутствие как недосмотр.
"""
docs, tasks = docs or {}, tasks or {}
out = [
"# Раскладка av-dev в этом проекте: версия и настройки проверок.",
"# Файл ведут скиллы плагина, править руками можно — комментарии свои.",
"",
f"{VERSION_KEY} = {number}"
" # версия раскладки; обратной совместимости нет, есть «приведён» и «нет»",
"",
"[docs]",
]
if docs.get("migrations"):
out += [
"# каталог миграций: по нему docs.py сверяет схему с database.md",
f"migrations = {quote(docs['migrations'])}",
]
else:
out += ['# migrations = "путь/к/миграциям" — появится, когда появится БД']
out += ["", "[tasks]",
"# каталог задач от корня репозитория; имена частей — умолчания скрипта",
f"dir = {quote(tasks.get('dir', 'tasks'))}"]
if tasks.get("stage"):
out += ["# стадия проекта: build — беклог это план стройки, порядок строк"
" значит зависимость;",
"# support — беклог это очередь правок, порядок значит важность",
f"stage = {quote(tasks['stage'])}"]
for key in ("items", "backlog", "rejected"):
if tasks.get(key):
out.append(f"{key} = {quote(tasks[key])}")
return "\n".join(out) + "\n"
+339
View File
@@ -0,0 +1,339 @@
# Язык проектных текстов
**Это дом.** Файл не принадлежит ни одному скиллу: язык общий для документов
канона, для задач и для решений ADR, и хранить его внутри одного из них значило
бы отдать общее правило во владение части. Скиллы читают **этот файл** по
ссылке; дословные **копии**, помеченные разметкой `copies.py`, уезжают только в
уставы вычитки — там текст обязан лежать внутри самого промпта, потому что
именно он и есть критерий суждения. Расхождение ловит гейт коммита, а не
внимание.
Правится **здесь**. Копия, поправленная у себя, — расхождение, а не правка.
Два блока копируются, и делятся они по потребителю, а не по теме:
| Блок | Что в нём | Кто копирует |
| --- | --- | --- |
| `язык-правила` | правила, по которым судят текст | уставы вычитки |
| `порог-правки` | когда находка не заводится | уставы вычитки, `task-form` |
`порог-правки` вынесен из правил намеренно: он нужен и тому, кто правил языка не
проверяет вовсе, — а вложенных блоков разметка не знает, и внутри `язык-правила`
его было бы не забрать отдельно.
Правила — для всего, что пишется словами **в этом плагине**: задачи, документы
канона, решения ADR, записки разведки. Не для кода и не для сообщений программы
пользователю — там свои конвенции проекта.
**Сообщения коммитов сюда не входят, и это названо намеренно.** Их форму держит
отдельный плагин `av-dev-git`, скилл `commit`, и она с этими правилами
расходится по существу: там предписан результат страдательным залогом
(«добавлены», «обновлено»), здесь — действие активным. Расхождение осознанное:
строка коммита отвечает на «что стало», а не на «что я сделал», и читают её в
`git log` подряд сотнями. Объявлять юрисдикцию над чужим плагином, до которого
отсюда нет и ссылки, значило бы завести правило, нарушение которого никто не
увидит.
Основа — **информационный стиль** Максима Ильяхова ([учебник
бюро](https://bureau.ru/projects/book-text/), книга «Пиши, сокращай»). Он
написан для рекламы, статей и писем, поэтому взят не целиком.
## Зачем он здесь
Проектный текст читают в двух положениях, и оба неудобные: **выбирают, брать ли
задачу**, глядя в строку индекса и один экран тела; и **возвращаются через
квартал**, не помня контекста. Оба положения наказывают одно и то же — слова, не
несущие сведений. Информационный стиль ровно про это, и его польза здесь не
эстетическая: текст, из которого нельзя достать факт, заставляет открывать код,
а это и есть цена, которой мы избегаем.
## Образец: научно-популярная книга
**Так, как пишут хорошую научно-популярную книгу.** Не спецификация, не статья в
блоге, не конспект для себя: текст, который объясняет устройство **точными
простыми словами** и понятен с первого прохода тому, кто эту систему не писал.
Из образца следуют три умолчания, и все три — про плотность, а не про красоту:
- **воды нет.** Каждая фраза несёт сведение: что устроено так, почему так и что
из этого следует. Абзац, из которого ничего нельзя достать, вычёркивается
целиком, а не переписывается;
- **сложных конструкций нет.** Причастный оборот внутри придаточного, три
отрицания подряд, предложение на пять строк — читатель разбирает такую фразу
дважды, и второй раз он её уже не разбирает. Причинную связь при этом не
режут: «поэтому», «иначе», «раз так» — сведения;
- **англицизм — исключение, требующее причины.** Умолчание обратное принятому в
разработке: пишем по-русски, а иностранное слово остаётся, только когда оно
**имя вещи** или когда русский аналог искажает смысл. Какая причина годится,
разбирает правило 5; закрытый список принятых слов — правило 6.
Термин здесь не запрещён — запрещена **перегрузка**: термин, который вводится
одной строкой, дешевле описания в три предложения, а термин, который
предполагается известным, дороже обоих (правило 8).
**Образец находок не порождает.** Он для того, кто пишет; вычитка судит по
правилам, и правка без нарушенного правила не делается (раздел «Порог правки»).
Иначе «мне кажется, звучит сложно» стало бы находкой, и список замечаний
перестали бы читать целиком.
## Что взято сверх правил вычитки
Эти три требования судит человек, а не проход вычитки: находка по ним требует
увидеть текст целиком, а не фразу.
**Полезное действие.** У каждого текста есть вопрос, на который он отвечает, и
читатель, который этот вопрос задаёт. Не отвечает — не пишется. У задачи это
«зачем она нужна» и «что станет наблюдаемо иначе»; у документа канона — его
собственный вопрос («что это за система», «как сложено», «почему так решили»).
Текст, который не отвечает ни на чей вопрос, сокращается до нуля — это законный
исход правки.
**Параллельность.** Однородное пишется одинаково: пункты списка — одной
грамматической формой, разделы одного вида — одним порядком, заголовки одного
уровня — одним типом фразы. Разнобой читатель принимает за разницу по существу и
ищет её.
**Заголовок работает.** Заголовок называет содержание раздела, а не тему
вообще: «Что проверяет `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`. Транслит
нечитаем тому, кто ищет по смыслу, и не сокращается, а имя стоит в ссылках,
коммитах и путях, которые набирают руками. Переименование — **перенос ссылок
одним проходом**, а не правка одного файла.
10. **Счёт корпуса не пишется словами.** «Пять ревью», «три capability»,
«десять проходов», «четыре документа канона» — это факт о корпусе, а дом у
такого факта сам корпус. Переписанный в прозу, он расходится с ним на первом
же пополнении, и расходится **молча**: фраза остаётся грамматически исправной
и правдоподобной, а проверить её можно только пересчётом, которого никто не
делает.
Сослаться можно двумя способами, и ни один не стареет:
| Как | Пример |
| --- | --- |
| на конкретную запись — именем, слагом, датой | «ревью от 3 августа», `adr/0007-queue-as-table.md`, capability `recognition` |
| на корпус целиком | «ревью проекта», «capability, объявленные в `openspec/specs/`» |
Величина нужна читателю редко, а когда нужна — её называет сам корпус в
момент чтения: каталог, индекс, команда. Абзац её только запоминает.
**Замер с названным происхождением — не счёт корпуса.** «Прозаический триггер дал 6
записей ADR на 43 изменения» описывает прошлое, а прошлое не пополняется: это
факт по правилу 2, и трогать его нельзя. Разница проверяется вопросом
«изменится ли число само, без правки текста».
**Число, стоящее заголовком к перечню, приведённому тут же, правилом не
задето.** «Три исхода:» с тремя пунктами под ним — не ссылка на корпус, и
расходится оно не втихую, а вместе со списком, который правят в той же
строке. Уехал перечень в другой файл — число уезжает с ним, а на его месте
остаётся ссылка.
<!-- /дом: язык-правила -->
## Порог правки
<!-- дом: порог-правки -->
**Правка без нарушенного правила не делается.** Текст, переписанный «чтобы
звучало лучше», обесценивает список замечаний: когда половина из них вкусовая,
перестают читать весь список, и вместе с ним пропадают настоящие находки.
Сомневаешься — не правь. Формулировка, которая просто **не твоя**, — не находка.
**Систематичность нарушения — не довод в его пользу.** Одна и та же ошибка в
пяти файлах не становится «принятым стилем»: чаще это значит, что правило не
применялось вовсе, — и находка тем важнее. «Так сделано везде» годится как
основание для **одной находки на весь набор** («правило N нарушено в пяти
записях, перечень: …»), но не как основание промолчать. Принятым считается
только то, что назвал зовущий или что записано в конвенциях проекта.
<!-- /дом: порог-правки -->
И обратное: язык правится **по ходу той операции, которая записи касается**.
Беклог не переписывают ради языка.
## Доклад вычитки
Не правило языка, а **контракт прохода**: форма, в которой находка приходит к
человеку. Живёт здесь потому, что проходов вычитки два — `doc-wording` по документам и
`task-wording` по записям задач, — и разойтись формой они не должны.
<!-- дом: вычитка-доклад -->
Находки по одной, в порядке важности: залог и оценки → жаргон и англицизмы →
стоп-слова. Первые меняют, **что** читатель понимает; последние — только сколько
он на это тратит.
```
<файл>
правило: <номер и короткое имя>
сейчас: <как написано>
предложение: <готовая формулировка, подставляемая как есть>
почему: <одна фраза>
```
В конце — **границы покрытия**: сколько файлов просмотрено из скольких, какие не
смотрел и почему, и по чему проверялись термины (документы проекта названы или
нет). Отчёт без этой строки читается как «всё вычитано», не сообщая, какая часть
осталась нетронутой. Туда же — строка «замечено не по моей части»; машинно
проверяемое в неё **не идёт**.
Ничего не нашёл — так и скажи одной строкой. Пустой доклад с границами покрытия
полезнее выдуманной находки.
<!-- /дом: вычитка-доклад -->
+35
View File
@@ -0,0 +1,35 @@
# Сопровождение и эксплуатация
**Это дом.** Словарь «чем держат проект» назван в трёх местах трёх разных
скиллов: задачи типа `chore` (`task-track`), раздел «Эксплуатация»
в `architecture.md` (`canon`) и тема ревью `operations` (`code-review`). Ни
один из трёх им не владеет, поэтому дом стоит в `shared/`.
Три перечня «чем держат проект» уже разъезжались — на «метриках и логах» против
«мониторинга», — и разъехались молча. Пока скиллы жили тремя плагинами, отсюда
уезжали дословные копии: путь в чужое дерево не разрешался. Теперь дерево одно —
кому словарь нужен, тот открывает **этот файл**, и сверять машиной больше нечего.
Одна тема живёт в трёх местах, и путать их слова нельзя.
**Сопровождение** — всё, чем держат проект: инструмент и сборка, процесс,
выкладка, метрики и логи, инфраструктура, дежурство. **Эксплуатация** — его
часть: работа системы на проде. Целое и часть, и никогда наоборот.
| Место | Уровень | Что там |
| --- | --- | --- |
| `BACKLOG.md`, задачи `chore` | план | **работы**, которые собираемся делать |
| `architecture.md`, раздел «Эксплуатация» | состояние | **как устроено сейчас**: где работает, что рядом, кто перезапускает |
| тема ревью `operations` | оптика | **чем проверяем**: «это упало через неделю на проде» |
Слово **«поддержка» не употребляется вовсе** — в нём слышится помощь
пользователю, а это другая работа. По той же причине им не названа и **стадия
проекта**: у неё имя «доработка» (`support`), словарь — `task-track`, «Две
стадии».
**Граница с возможностями проходит по тому, кто наблюдает.** «Приложение
сообщает о своём состоянии» — возможность приложения, её место среди прочих
задач: наблюдает пользователь сервиса. «Дежурный видит состояние на одном
экране» — сопровождение: наблюдаем мы. Одни и те же метрики попадают в задачи
разных типов, и это верно — типы отвечают на разные вопросы.
+357
View File
@@ -0,0 +1,357 @@
---
name: canon
description: Форма раскладки проекта под av-dev и её обновление — три операции одной машиной сравнения. check — что разошлось с текущей версией раскладки; adopt — перевод проекта из любой прежней раскладки (docs/specs, drafts, backlog, BRIEF.md, review-brief) в канон с переносом файлов и вызовом владельцев каталога задач и openspec/; upgrade — повышение проекта с версии N до текущей по журналу версий, и повышается им вся раскладка, включая каталог задач. Использовать, когда просят проверить документацию проекта, перевести проект на канон, обновить его под новую версию раскладки или когда пришли в старый проект и надо понять, что в нём не так. Имя без префикса намеренно — скилл держит форму всех артефактов проекта, а не один их вид. Содержимое документов ведёт av-dev:doc-sync, форму записей задач — av-dev:task-track, заведение проекта с нуля — av-dev:doc-init.
---
# Форма раскладки проекта
Три операции, одна машина сравнения с разными исходами:
| Операция | Когда | Исход |
| --- | --- | --- |
| `check` | начало сессии, шаг синка, гейт | что разошлось |
| `adopt` | проект в чужой раскладке | перенос в канон |
| `upgrade` | канон вырос, проект отстал | по журналу версий |
**Имя без префикса, и это не случайность.** Остальные скиллы названы по
материалу, с которым работают, — `doc-`, `task-`, `code-`; этот работает не с
материалом, а с **формой**, и она у всех частей проекта одна. `check` сверяет
раскладку документов, `adopt` заводит все части сразу и зовёт владельцев каталога
задач и `openspec/`, `upgrade` повышает **всю** раскладку одним журналом версий —
и документы, и каталог задач. Содержимое при этом не его: документы ведёт
`av-dev:doc-sync`, записи задач — `av-dev:task-track`.
**Определение канона — [references/canon.md](references/canon.md).** Здесь оно не
пересказывается: два описания одной раскладки разъедутся, и работать будет то,
которое прочитали последним. Прочитай его **до** первой правки.
- [references/skeletons.md](references/skeletons.md) — **что именно класть** в
каждый незаполненный слот. Не выдумывай заглушку своей формы: `docs.py`
узнаёт только плейсхолдер `<!-- заполнить: … -->` из шаблонов.
- [shared/language.md](../../shared/language.md) — **как это написано словами**:
информационный стиль, применённый к проектным текстам, таблицы англицизмов и
жаргона. Раскладка отвечает, где текст лежит, — этот файл отвечает, каким он
должен быть. Правила общие для документов канона, задач, решений ADR и
записок разведки, и это их **дом**. Вычитывают их два прохода по охвату:
документы — `doc-wording`, записи каталога задач — `task-wording`.
- [references/changelog.md](references/changelog.md) — журнал версий раскладки;
закрытые журналы до слияния плагинов лежат рядом.
## Три правила, из которых всё следует
1. **Сперва карта, потом файлы.** Человеку показывается, что найдено, как
разложилось и **что не разложилось**, — и только после подтверждения
переносится хоть один файл. Массовый перенос без подтверждения разгребать
дороже, чем согласовать.
2. **Ничего не терять.** Содержимое переезжает целиком; ссылки чинятся тем же
проходом, что и перенос. Старый файл удаляется **только** после того, как
всё его содержимое нашло дом, и это названо поимённо.
3. **Что не классифицировалось — назвать.** Проглоченный абзац выглядит как
«всё перенеслось». Список «не разложилось» идёт в доклад целиком, с причиной
по каждому пункту.
## Инструмент
```
ds="$CLAUDE_PLUGIN_ROOT/skills/canon/scripts/docs.py"
python3 $ds check --dir <корень> [--base <rev>] # раскладка, ссылки, версия, сверки
python3 $ds version --dir <корень> # версия раскладки: скрипта и проекта
python3 $ds bump --dir <корень> # поднять версию проекта до версии скрипта
```
**Формы `openspec/config.yaml` здесь больше нет.** Каталог принадлежит конвейеру,
и форму смотрит его скрипт — скилл `av-dev:code-openspec`, команда
`openspec.py check`. Проект работает по OpenSpec, а каталога `openspec/` нет — форму
не проверяет никто, и это надо сказать строкой доклада, а не считать, что она
верна.
**Копия.** Дом словаря — `shared/axes.md` в репозитории плагина: словарь общий
для всех скриптов, и ни один скилл им не владеет. Правится дом, а не этот файл.
<!-- копия: коды-выхода из av-dev/shared/axes.md -->
**Коды выхода — общий словарь всех скриптов `av-dev`. Ветвись на коде, а не на
тексте вывода.**
| Код | Что случилось |
| --- | --- |
| 0 | сошлось |
| 1 | дрейф: рабочая ситуация, чинится |
| 2 | ошибка употребления: аргументы или нарушенное правило |
| 3 | окружение: не тот каталог, битый конфиг, нет инструмента |
| 4 | внутренний сбой — дефект скрипта, доложить |
**Различать 1 и 3 обязательно.** «Дрейф» — рабочая ситуация, и чинится она
правкой предмета; «окружение» — нерабочая, и повтор той же командой не поможет.
Одинаковая реакция на них неверна в обоих случаях.
<!-- /копия: коды-выхода -->
Здесь это значит: «дрейф раскладки» — рабочая ситуация, «это не корень проекта» —
нерабочая.
### Граница механизируемого — объявляется вслух
Скрипт печатает её сам последним абзацем, и **эту строку из доклада выбрасывать
нельзя**. `check`, отчитавшийся «канон соблюдён» на проекте, где из шести файлов
три лишние, хуже отсутствующего.
Машина **дрейфом** считает: отсутствующий путь канона, файл вне канона, битую
ссылку, отставшую версию, capability без упоминания в обзоре, миграцию без правки
`database.md`. **Замечанием** — незаполненный плейсхолдер и слабое упоминание
capability: незаполненный канон это переходное состояние, а не отказ. Маркеры
долга просто считает числом.
Того, чего она не умеет, **ты не судишь сам** — для этого есть два агента, и
разведены они по глубине:
| Агент | Что смотрит | Читает |
| --- | --- | --- |
| `doc-consistency` | смысловой дубль, прямое противоречие между документами, поведение в `architecture.md` вместо спек, ADR без ссылки и парного статуса, число без происхождения, заглушка вместо честной строки | `docs/`, `openspec/` |
| `doc-code-drift` | протухший факт: имя ветки, команды, пути, зависимости поимённо, настройки с числом, единые точки проекта, capability | весь репозиторий |
Судит **не тот, кто писал**: самопроверка документа слабее всего ровно там, где
формулировка казалась удачной при написании. Ни один из них ничего не правит —
оба возвращают готовые формулировки, подставляешь ты.
## Чего может не быть
`adopt` зовёт двоих: `av-dev:code-openspec` (шаг 4, пункт 3) и
`av-dev:task-track` (шаг 4, пункт 5). Каталоги `openspec/` и `tasks/` каноном не
ведутся, и трогать их этому скиллу нечем, кроме вызова.
**Копия.** Дом правила — `shared/absence.md` в репозитории плагина.
Правится дом, а не этот файл.
<!-- копия: отсутствие из av-dev/shared/absence.md -->
**Скилл не вправе считать раскладку проекта полной.** Части заводятся порознь и
живут порознь; каждая узнаётся своим следом:
| Чего нет | Как видно | Чего теперь не делает никто |
| --- | --- | --- |
| настройки av-dev | нет `.av-dev.toml` в корне | проект под процесс не заводился; версии нет, настроек нет |
| документы канона | нет `docs/` | проектную конкретику брать неоткуда — темы, инварианты, прецеденты |
| учёт работ | нет каталога задач | запись остаётся владельцу: назови её текстом в докладе |
| источник требований | нет `openspec/config.yaml` | цикл SDD не запускается: спеки не с чем сверять |
**Свой скилл зовётся полным именем** — `av-dev:canon`, `av-dev:task-track`,
`av-dev:code-review`. Короткое имя может разрешиться в устаревшую проектную
копию из `.claude/skills/`, и подмены не будет видно ни в докладе, ни в
поведении.
**Внешний плагин может не стоять.** Их два: `opsx:*` — цикл SDD, и
`av-dev-git:commit` — сообщения коммитов. Путь в дерево чужого плагина не
пишется никогда: `$CLAUDE_PLUGIN_ROOT` ведёт только в своё дерево, а
вычисленный от него путь к соседу либо не откроется, либо откроет чужую
установку. Нужен чужой справочник — зови владеющий им скилл, он прочитает его
сам.
**Отсутствие — исход, а не поломка.** Назови строкой доклада, чего теперь не
делает никто, и продолжай работу. Молчать нельзя: пропуск неотличим от
сделанного. Выдумывать обходной путь нельзя тоже.
**Присутствие узнаётся следом в проекте, а не объявлением.** Перечня того, что
здесь заведено, проект не ведёт — он разошёлся бы с действительностью молча.
<!-- /копия: отсутствие -->
Чем оборачивается отсутствие каждого — на самих пунктах шага 4. `adopt` из-за
этого не останавливается ни в одном из двух случаев.
## `check`
1. `docs.py check`, при наличии базы диффа — с `--base`.
2. **Судей документов на каждом `check` не зови.** Ими владеет отдельный скилл —
`av-dev:doc-healthcheck`, — и там же записано, когда его звать: он дорог, и
прогон по каждому `check` не окупается. `check` отвечает на «сходится ли
форма», `doc-healthcheck` — на «не разошлись ли утверждения».
3. Доклад: вывод скрипта строкой исхода и **граница покрытия** — что смотрели и
чего не смотрели. Если суждение здесь нужно, скажи это строкой и предложи
`doc-healthcheck`, а не зови агентов сам.
Дрейф раскладки чинится переносом; смысловые находки — это либо правка
документа, либо задача, если работы больше чем на абзац.
## `adopt` — проект в чужой раскладке
### 1. Осмотрись
`docs.py check` — он уже назовёт упразднённые слоты с адресом, куда каждый
уезжает. **Но смотрит он только верхний уровень `docs/`:** упразднённое в корне
репозитория (`BRIEF.md`) и во вложенных каталогах он не назовёт никогда, поэтому
корневые `*.md` читай глазами. Плюс: `CLAUDE.md`, `openspec/specs/` (список
capability), `openspec/config.yaml`.
### 2. Составь карту
Каждый найденный файл получает строку: **куда едет, целиком или разбирается, что
делать с оригиналом**. Разбор `docs/specs/` — самое дорогое место, и он делается
поимённо по capability:
| Что в файле | Куда |
| --- | --- |
| требования, сценарии, поведение | `openspec/specs/<capability>/spec.md`**или уже там**, тогда файл дубль |
| компоненты, транспорты, раскладка, деплой | `docs/architecture.md` |
| конвенции чужой системы, формат чужих данных | `docs/research/` |
| обоснование принятого решения | `docs/adr/` |
**Дубль удаляется только после поимённой сверки**: открыть спеку capability,
открыть файл, убедиться, что в файле нет ничего сверх спеки. Нашлось сверх —
сперва переезжает в спеку дельтой, потом файл удаляется.
### 3. Покажи карту человеку
`AskUserQuestion`, **не больше трёх вопросов за итерацию**, рекомендация первым
вариантом. Показывается: сколько файлов, куда каждый, спорные отнесения, список
«не разложилось». Механику (порядок строк, имена файлов внутри `research/`) не
выноси — это не развилка.
### 4. Перенеси
Порядок важен — он минимизирует окно, в котором ссылки битые:
1. `.av-dev.toml` в корне: `version = <текущая версия>` и путь миграций в
`[docs]`, если БД есть;
2. каталоги канона и скелет **по [references/skeletons.md](references/skeletons.md)**:
незаполненное — одной честной информативной строкой, а не «TBD»;
3. **OpenSpec, если его нет или `config.yaml` остался примером** — **вызови
Skill `av-dev:code-openspec`**. Каталог принадлежит конвейеру, и команда
заведения с формой файла живут там. Пересказ инвариантов, конвенций и правил
ревью из `context` вычисти ссылкой на дом — на переводимом проекте он там
почти наверняка есть. Проект решил жить без OpenSpec — `docs.py` о каталоге
тогда тоже молчит, и форму `config.yaml` не проверяет никто; скажи это
строкой;
4. переносы содержимого;
5. каталог задач — **вызови скилл `av-dev:task-track`**, сценарий адаптации: он
владеет форматом задач. Он же переименует транслитные слаги в английские и
тем же проходом починит перекрёстные ссылки;
6. починка ссылок на перенесённое во всём репозитории — `docs/`, `openspec/`,
`CLAUDE.md`, `README.md`;
7. удаление оригиналов — **только тех, чьё содержимое найдено в новом доме**;
8. **шаг `docs.py check` в гейт проекта.** Путь к скрипту — переменной с
умолчанием на канонический путь маркетплейса, чтобы переустановка плагина не
меняла `Taskfile`; шаг обязан **краснеть внятно**, если скрипт не найден, а не
пропускаться. Передай ему базу диффа (`--base`) той же переменной, что и
остальным шагам гейта: без неё сверка миграций со схемой не гоняется вовсе.
Пример строки покажи человеку — гейт принадлежит проекту, и правит его он.
**Шагов в гейте три, и они независимы.** `docs.py check` не тянет за собой
ни задачи, ни конвейер: без своих строк дрейф каталога задач и формы
`openspec/config.yaml` перестаёт ловиться совсем. Ставь соседние шаги по
следу присутствия — каталог задач с индексом на месте, значит ставится
`tasks.py check --dir <каталог задач>`; `openspec/config.yaml` есть, значит
ставится `openspec.py check`. Следа нет — этой части в проекте нет, шаг не
ставится, и это **строка доклада**, а не поломка: назови, чего теперь не
проверяет никто. У каждого шага своя переменная пути с тем же умолчанием на
канонический путь маркетплейса; `$CLAUDE_PLUGIN_ROOT` в гейт не подставляй —
он ведёт только в свой плагин;
9. `docs.py check` — до **отсутствия дрейфа раскладки**. Замечания
(незаполненные плейсхолдеры, слабое упоминание capability) остаются:
незаполненный канон это объявленное переходное состояние из шага 5, а не
отказ. Пересчитай эти пункты в докладе переходного состояния — не выдавай
их за поломку и не молчи о них.
**Задачи `docs.py` не проверяет** — их ведёт другой скилл, и согласованность
каталога показывает только `tasks.py check`. Позвал на шаге 5 скилл задач —
его отчёт идёт в доклад отдельной строкой, и зелёным он сразу не станет:
у перенесённых записей нет критериев приёмки, а `check` без объявленной
**стадии** отказывает вовсе. Стадию называет человек (`tasks.py stage
build|support`) — машина её не выводит: список пунктов одинаково выглядит и
планом стройки, и очередью правок.
### 5. Объяви переходное состояние
Сразу после переноса канон **заполнен не весь**, и это нормально, но обязано
быть названо, иначе следующий агент примет скелет за поломку.
Печатается по факту: сколько документов стоят честной строкой вместо
содержания, сколько маркеров долга в `architecture.md`, сколько задач без
критериев приёмки. Закрывается порциями по ходу работы, а не одним заходом.
### 6. Позови обоих судей
`docs.py` увидел раскладку, а не смысл: перенос растащил один факт по двум домам,
оставил в `architecture.md` поведение, которому место в спеке, и оторвал ADR от
его `design.md`. Ничего из этого скрипт не видит, и первый прогон на живом
проекте обычно самый урожайный — правило единственного дома до адаптации никто не
проверял.
Вызови Skill **`av-dev:doc-healthcheck`** — он зовёт обоих судей на весь канон
разом и держит разбор урожая порциями.
**Передай им объявленное переходное состояние из шага 5** — иначе честная строка
в незаполненном слоте вернётся находкой, а это не поломка, а объявленный долг.
### 7. Вычитай написанное — агент `doc-wording`
Судьи смотрят утверждения, а `adopt` только что **писал текст**: честные строки
в пустые слоты, переписанные при переносе абзацы, шапки перенесённых документов.
Язык этого текста не проверяет никто другой, а зовущий здесь по определению тот,
кто его и написал.
Позови агента **по названной пачке** — документы, которые ты завёл или правил,
плюс перенесённые целиком. Весь канон ему не нужен: он работает по списку, и
список же служит ему словарём терминов. Находки — готовые формулировки,
подставляешь их ты.
## `upgrade` — канон вырос
1. `docs.py version` — версия проекта и версия скрипта.
2. Проект новее скрипта — **обнови маркетплейс**, а не проект: это отстал
плагин.
3. Иначе иди по [changelog.md](references/changelog.md) снизу вверх от версии
проекта до текущей и делай названное в каждой записи. Записи независимы и
применяются по порядку.
4. Подними версию — `docs.py bump`. Он правит **строку**, а не переписывает
файл: комментарии в нём принадлежат проекту. Последним шагом, потому что
число объявляет пройденными записи журнала.
5. `docs.py check`.
6. **Позови судей** — Skill `av-dev:doc-healthcheck`.
7. **Позови вычитку** — агент `doc-wording`, но **только по тем документам,
которых записи журнала коснулись**, и только если правка была текстовой, а не
переименованием файла. Записи журнала пишутся руками в проектной прозе, и
дописанный по журналу раздел — такой же свежий текст, как на синке.
Записи журнала описывают **что сделать проекту**. Если запись этого не говорит —
это дефект журнала, и о нём надо сказать, а не догадываться.
**Каталог задач повышается этим же журналом.** Версия одна на всю раскладку —
`version` в `.av-dev.toml`, — и записи журнала говорят про обе половины: и про
документы, и про каталог задач. Порознь версии жили, пока плагинов было три и
проект мог взять одну половину без другой; с одним плагином два числа означали
бы только вопрос, по какому журналу повышать. Что каталог задач отстал, скажет
`tasks.py check` своей строкой гейта — той же версией, что и `docs.py`.
**Шаг 6 обязателен, и вот почему.** `check` сверяет **число** в `.av-dev.toml`
с версией скрипта — и только его. Применена ли запись журнала **по существу**,
он не знает: проект несёт `version` текущей версии и может не иметь того, чего требовала любая
из пройденных версий. Записи применяются руками (переименовать секцию, проставить
типы, дописать раздел каждому `fix`), а ручной проход по нескольким записям
подряд — ровно то место, где половина шага делается и забывается. Судьи и есть
проверка, которой у `upgrade` иначе нет: `doc-consistency` увидит, что документы
разошлись после переименований, `doc-code-drift` — что переехавший факт
разошёлся с кодом.
## Чего этот скилл не делает
- **Не сочиняет содержание.** Пустой слот получает честную строку о том, что его
наполнить пока нечем, а не выдуманный абзац. Придуманный периметр модели угроз
хуже отсутствующего: по нему будут строиться находки.
- **Не удаляет то, чьё содержимое не нашло дом.** Оригинал живёт, пока не
названо поимённо, куда переехал каждый его кусок.
- **Не ведёт содержимое канона** — это скилл `doc-sync`. Здесь только раскладка.
- **Не заводит проект с нуля** — это скилл `doc-init`.
- **Не правит историю.** В старых коммитах старые пути остаются, и это нормально.
## Доклад
- Что нашёл `docs.py`: код выхода и число пунктов дрейфа.
- Что перенесено: файл → дом, числом и поимённо для спорного.
- **Удалённые дубли** — с указанием, против какой спеки сверялся каждый.
- **Не разложилось** — поимённо, с причиной.
- Переходное состояние числами: честных строк, маркеров долга, задач без
критериев.
- **Граница покрытия**: что проверила машина, что судил ты, чего не смотрел
никто.
+619
View File
@@ -0,0 +1,619 @@
# Канон документов проекта
**Номер версии здесь не стоит намеренно.** Этот файл описывает канон таким, какой
он сейчас, а число живёт в двух домах, которые не расходятся: константа в
`docs.py` (её печатает `docs.py version`) и верхняя запись
[журнала](changelog.md). Литерал в шапке был третьим и отстал на первом же
повышении — версию 13 он пережил, объявляя канон двенадцатым.
Это **единственный дом определения канона**. Скиллы `doc-init`, `canon` и `doc-sync`
читают его, а не пересказывают: три описания одной раскладки разъедутся, и
работать будет то, которое прочитали последним. Меняется канон — меняется этот
файл и появляется запись в [changelog.md](changelog.md).
## Зачем канон жёсткий
Пути фиксированы, и проект под них подгоняется, а не наоборот. Причина не
техническая: проектов много, все малого и среднего размера, и ориентироваться в
слегка похожих, но разных раскладках дороже, чем один раз привести их к общей.
Рядом лежит OpenSpec, у которого структура тоже строгая.
Цена принята сознательно: плагин не переносится на чужой репозиторий как есть —
чужой репозиторий **приводится** к канону скиллом `canon`.
Раскладка отвечает, **где** текст лежит и на какой вопрос отвечает. Каким он
должен быть **словами** — общий для всех документов канона файл
[shared/language.md](../../../shared/language.md): информационный стиль, англицизмы, жаргон. Он
относится и к задачам, и к решениям ADR, и к запискам разведки.
## Сопровождение и эксплуатация — целое и часть
Словарь этой темы — [shared/operations.md](../../../shared/operations.md):
целое и часть, три места одной темы (задачи `chore`, раздел архитектуры, тема
ревью `operations`) и граница с возможностями проекта. Здесь он не
пересказывается: копия жила рядом с домом в одном дереве и была ровно тем
вторым домом, против которого правило и написано.
## Раскладка
**Документ канона живёт файлом или каталогом.** `docs/security.md` и
`docs/security/` — одно и то же; форму выбирает проект по объёму написанного, и
переход между формами не меняет ни канон, ни версию. Обе формы сразу — ошибка:
два дома для одного факта расходятся молча.
```
CLAUDE.md памятка агенту: что это, стек, инварианты с
severity, команды, семантика гейта, запреты
AGENTS.md необязателен, лежит рядом; читается теми же
.av-dev.toml версия раскладки и настройки проверок; лежит
в корне, потому что нужен и без docs/
docs/
passport.md | passport/ зачем и для кого; чем НЕ является; сценарии
architecture.md | architecture/ как сложено — обзор; окружение и эксплуатация
database.md | database/ схема хранилища; представление данных и настройки
security.md | security/ периметр; недоверенный вход; что вне модели
conventions.md | conventions/ как пишем код; что механизировано
research.md | research/ наблюдения и числа с происхождением
adr.md | adr/ почему решено так; статусы, правило замены
review.md | review/ настройка конвейера под проект + журнал дефектов
<своя тема>.md | <своя тема>/ всё, что проект счёл нужным проверять
tasks/ каталог задач — скилл task-track, не канон;
лежит в корне, вне docs/, и канон его не требует
openspec/
config.yaml только нужды генерации артефактов + ссылки
specs/<capability>/spec.md что система делает — нормативно
changes/archive/ архив изменений с design.md — сырьё для ADR
```
**У документа-каталога обязателен `README.md`** — вход, по которому его читают агенты.
`adr/` в форме каталога держит ещё и `template.md`, а записи именуются
`ADR-ГГГГ-ММ-ДД-slug.md`.
## Три категории документов
Категория документа — ось процесса; перечень осей и того, чего каждая **не**
решает, — [shared/axes.md](../../../shared/axes.md).
Раньше здесь стояло плоское правило «каждый документ `docs/` — тема ревью». Оно
неверно ровно наполовину: паспорт и схема хранилища ревью нужны, но темами не
являются, а журнал решений и журнал наблюдений ревью изменения не нужны вовсе.
Плоское правило заставляло прогон либо плодить фантомные темы, либо терять
документы молча — а молчащая потеря и есть то, против чего канон написан.
**Разрез один и проверяемый: можно ли по документу сказать «в этом изменении
сделано не так»?**
| Категория | Ответ на разрез | Что с ней делает ревью |
| --- | --- | --- |
| **тема** | да, прямо | заводит направление проверки и требует исполнителя |
| **источник темы** | нет, но он задаёт границу, по которой судит чужая тема | читается как материал, своей темы не порождает |
| **процессный документ** | нет: он про то, как мы работаем, а не про изменение | не судит по нему изменение |
| Документ | Категория | Куда питает |
| --- | --- | --- |
| `conventions.*` | тема | `conventions` |
| `security.*` | тема | `security` |
| `architecture.*` | тема | `architecture`; раздел эксплуатации — `operations` |
| *свой документ проекта* | тема | своя тема, именем документа |
| `passport.*` | источник | `architecture` — граница домена, «чем **не** является» |
| `database.*` | источник | `operations` — схема и настройки с числами |
| `CLAUDE.md`, `AGENTS.md` | источник | `autotests` (семантика гейта); инварианты — сквозные |
| `openspec/specs/` | источник | `requirements` |
| `openspec/config.yaml` | процессный | — (настройка порождения артефактов, слой **до** тем; заводит конвейер) |
| `tasks/` | процессный | — (чужое владение: скилл `task-track`) |
| `review.*` | процессный | — (настройка самого конвейера, слой **над** темами) |
| `adr.*` | процессный | — |
| `research.*` | процессный | — |
| `.av-dev.toml` | процессный | — (служебный файл, не документ) |
**Список тем открытый, и это не послабление, а механизм.** Категории
`источник` и `процессный` **закрыты** — они перечислены здесь поимённо и
проектом не пополняются. Всё остальное, что проект кладёт в `docs/`, — тема: у
конвейера есть приёмник для темы, к которой нет именной оптики, и заведён он
ровно за этим. Завёл `docs/accessibility.md` — появилась тема `accessibility`, и
она попадает в план каждого прогона.
Отсюда следствие, ради которого правило и заведено: **`docs/` — это конфигурация
ревью.** Проект настраивает проверку тем, что пишет о себе, а не отдельным файлом
настроек, который разошёлся бы с документами.
**«Не судит по нему» и «не открывает» — не одно и то же, и разница существенна.**
`docs/review.*` проходы читают на каждом прогоне: там лежат вопросы по темам,
журнал дефектов, типовые узлы и типовые ложноположительные. Это чтение конвейером
**собственной настройки**, а не суждение об изменении, и потому оно законно.
`adr/`, `research/` и `tasks/` не открывает никто: по ним изменение не судят, и
настройкой конвейера они не являются.
**Процессный документ — не документ второго сорта.** `adr/` и `research/`
проверяются наравне с остальными, но **сверкой документации**, а не прогоном
ревью: ADR без ссылки на источник, замена без парного статуса, число
без происхождения — это работа агентов `doc-consistency` и `doc-code-drift`, и она
осталась там же, где была. Изменилось одно: прогон ревью не открывает их как
критерий и не судит по ним изменение.
Цена этого решения записана, а не подразумевается: **расхождение изменения с
записанным решением прогоном больше не ловится.** Раньше архитектурный проход
читал `adr/` и мог сказать «здесь отменено решение ADR-2026-03-11, а парного
статуса нет»; теперь это скажет только `doc-consistency`, а зовёт его скилл
`healthcheck`. Сделка сознательная — ADR объясняет прошлое, а не предъявляет
требование к изменению, и чтение всего каталога решений на каждой задаче
оплачивалось на каждой, а срабатывало на единицах.
### Имена файлов английские, текст русский
**Текст документов русский; имена файлов, capability и задач — английские,
kebab-case.** Причина не эстетическая: имя файла стоит в ссылках из других
документов, в коммитах и в путях, которые набирают руками, — а кириллица в пути
ломается по-разному в разных местах и не набирается на английской раскладке.
**Транслита не заводим.** Слаг именуется английским словом **по сути**, а не
записью русского латиницей: `queue-as-table`, а не `ochered-tablicej`. Транслит
нечитаем тому, кто ищет по смыслу, и не сокращается.
У ADR имя вдобавок несёт форму — `ADR-ГГГГ-ММ-ДД-slug.md`: по ней записи
сортируются, и по ней же ищется дата решения.
`docs.py check` проверяет кириллицу и kebab-case **жёстко**, форму имени ADR —
тоже, а транслит **эвристикой**, то есть замечанием: английское слово от
транслита машина не отличает. Слаги каталога задач ведёт `tasks.py` — там та же
проверка и тот же разрез.
**Переименование — не правка, а перенос ссылок**: делается одним проходом по
всем местам, где имя упомянуто, иначе останутся битые ссылки. Для задач это
умеет `tasks.py adopt`; для документов канона правит человек, а `docs.py` потом
показывает, что ссылки целы.
## Роли документов и темы ревью
Одна строка на каждый — на какой вопрос он отвечает, в какой он категории и
какую тему питает. **Кто именно закрывает тему, здесь не указано намеренно**: это
меняется вместе с конвейером, а документ живёт дольше. Раскладку «тема → кто
закрывает → против чего» держит скилл `av-dev:code-review`.
**Общего словаря у канона с конвейером два вида имён: имена категорий и имена
тем.** Категорий три — `тема`, `источник`, `процессный`; список тем открытый, и
пополняет его сам проект своим документом. Настраивает проект ревью двумя вещами:
вопросами по темам и признаками, по которым зовут глубокое ревью. **Имён проходов
канон не называет нигде**, включая вывод `docs.py`: проход переименовывается и
переезжает в другой скилл, и канон, назвавший его, в этот день соврёт молча.
Обратное направление законно — конвейер называет документы канона поимённо,
потому что он их читатель.
| Документ | Вопрос | Категория и тема |
| --- | --- | --- |
| `CLAUDE.md`, `AGENTS.md` | что нельзя нарушать, чем краснеет гейт | источник: `autotests`; инварианты — сквозные, во все темы |
| `passport.*` | зачем и для кого, чем это **не** является | источник: `architecture` |
| `architecture.*` | как сложено и где что работает | тема `architecture`; раздел эксплуатации — `operations` |
| `database.*` | что лежит в хранилище и какими настройками | источник: `operations` |
| `security.*` | против кого защищаемся и что вне модели | тема `security` |
| `conventions.*` | как мы пишем код | тема `conventions` |
| `openspec/specs/` | что система делает — нормативно | источник: `requirements` |
| `research.*` | что показала реальность, а не документация | процессный |
| `adr.*` | почему решено именно так | процессный |
| `review.*` | как настроен конвейер и что уже проскакивало | процессный: слой **над** темами |
| `tasks/` | что делаем и в каком порядке | процессный |
| *свой документ проекта* | что проект счёл нужным проверять | **своя тема**, именем документа |
### `passport.md`
Цель; закрытый список потребителей и что каждому нужно; **чем целью не
является** — это граница домена, по которой архитектурный проход судит о
переносе понятия; типовые сценарии; мера, по которой проект считается удавшимся;
референсы, у кого подсматривать.
### `architecture.md` — **обзор, не поведение**
Принципы; компоненты **со ссылками на capability**, а не с пересказом их
требований; **единые точки проекта** — где генерируются идентификаторы и время,
где единственный парсер входного формата, где маппинг доменной ошибки в код
ответа, где общий путь приёма (это материал для вопроса «не появился ли второй
способ»); внешние границы и форматы чужих систем; окружение — где работает, что
рядом, кто перезапускает; **внешние зависимости поимённо** и чем каждая
отказывает (не только «падает», но и «отвечает медленно», «молчит», «отдаёт
мусор»); кто заметит отказ и когда; характер потока — непрерывный, по запросу,
по расписанию; деплой; открытые вопросы.
**Обратимости здесь нет** — её единственный дом `CLAUDE.md`: туда ходят пять
проходов, и раздвоение адреса означало бы, что проект написал ответ, а ревью его
не прочитало.
**Поведение системы сюда не пишется.** Его нормативный дом — `openspec/specs/`,
куда `opsx:archive` вливает дельты; второй дом синхронизировать руками
невозможно, и он разойдётся.
Раздел, ещё не разнесённый при переезде, помечается маркером долга:
```
<!-- канон: поведение → openspec/specs/<capability> -->
```
`docs.py` считает маркеры и печатает остаток числом. Гейт от них **не краснеет**:
это долг, а не отказ, иначе постепенный переезд стал бы невозможен.
### `database.md`
Схема: таблицы, ключи, связи, правило времени и идентификаторов. Плюс то, чего
нет в схеме, но без чего замер не превращается в находку: **чем физически лежит
запись** (сжатый BLOB, JSON-строка, колонки), что происходит при чтении и записи
(распаковка целиком, read-modify-write), и **настройки с числовым значением**
таймаут занятости, режим журналирования, лимит тела, размер пула, ретеншен.
Конвенции идентификаторов и именования — не схема, они в `conventions/`.
### `security.md`
**Периметр первой строкой.** «Сервис открыт наружу» и «контур доверенный,
публичного интернета здесь нет, не выдумывай его» — противоположные постановки
под одним заголовком, и разбор темы `security` между ними сам не выберет. Контур ещё
не развёрнут — назови **оба** периметра, целевой и сегодняшний, и скажи прямо,
против какого строятся находки.
Дальше: что недоверенное и каким каналом приходит; **из чего строятся пути и
ключи** (раскладка файлов, состав координатного ключа, имя каталога) — отсюда
строится выход за пределы песочницы; что разграничивает доступ; что
чувствительнее чего; **что вне модели** — перечислить явно.
### `conventions/`
Прозой остаётся **только то, что не выражается правилом**. `README.md` держит
индекс, правило промоута и **перечень уже механизированного** со ссылкой на
место механизации — конфиг линтера, собственный анализатор, тест-сканер
исходников. Не названное место механизации означает, что проход добросовестно
проверит уже проверенное.
### `research/`
Наблюдения за внешним миром: что реально шлёт источник, чем документация формата
расходится с практикой, какие числа сняты с живого потока. **Числа — с
происхождением**, то есть с командой или условиями, которыми получены.
`README.md` — как снималось и индекс тем.
Число без источника проход обязан читать как условие, а не как замер. Число, чей
источник по ссылке не подтвердился, не выбрасывается и не переписывается по
догадке — остаётся с пометкой «расходится с источником: там <что нашли>».
### `adr/`
**ADR продвигает уже написанное решение, а не сочиняет его заново.** Запись
цитирует решение и ссылается на источник. Источников два, и оба законны:
- **архивный `design.md`** — решение принято по ходу изменения:
`openspec/changes/archive/<id>/design.md`. Обычный случай;
- **записка разведки** — решение принято разведкой, и change по нему не будет
никогда: намеренный отказ, выбор подхода, «проверили и не делаем». У такой
работы нет `design.md` по построению, и без второго источника её решение либо
не попадало в `adr/` вовсе, либо попадало сочинённым заново.
Источник называется в записи всегда — по нему видно, чем решение подтверждено.
Заводится, когда верно одно из трёх:
<!-- дом: adr-когда-заводить -->
- **дорогой откат** — переделка стоит дороже переписывания одного файла;
- **намеренный отказ** от очевидного подхода;
- **пересмотр прежнего решения** — тогда у старой записи обязателен статус
«заменено на».
<!-- /дом: adr-когда-заводить -->
**Сработавший триггер даёт предложение, а не запись.** Заводит ADR человек своим
словом — правило и его причина в скилле `av-dev:doc-sync`, раздел «Два рода
правок». Канон здесь отвечает за другое: за то, при каких условиях предлагать
вообще есть что.
Не заводится для рутины и для того, что видно из кода и `git log`.
Записи неизменяемы: передумали — заводится новая, старая получает статус.
Активная запись статуса не имеет.
**Статус живёт полем меты записи**, там же, где дата и источник:
`- **Статус:** заменено на ADR-…` либо `- **Статус:** устарело`. Места ему в
шаблоне не отводилось, и каждая запись изобретала своё — то абзацем, то
заголовком; в таблице `adr/README.md` статус при этом обязан быть, а брать его
оттуда, где он у каждого свой, нельзя.
### `review.md`
Два раздела с разными сроками жизни.
**Настройка конвейера под проект**, пять подразделов с точными именами — по ним
проходы находят свой кусок:
- **Типовые узлы** — рода узлов проекта и 3–5 проверяемых свойств к каждому;
- **Типовые ложноположительные** — находки, которые здесь выглядят убедительно и
всегда неверны, каждая со строкой «почему здесь это не дефект»;
- **Вопросы по темам** — в форме `<тема>: <вопрос> (<откуда>)`. **Не по именам
проходов**: проход уезжает в другой скилл, а тема остаётся, и вопрос,
адресованный проходу, перестал бы задаваться молча в тот день, когда тот уехал.
Задаёт вопрос тот, кто закрывает тему на этом прогоне.
Адресовать можно только теме: `passport`, `database`, `adr`, `research` и
`review` — не темы, и вопрос, адресованный им, не задаст никто;
- **Когда звать глубокое ревью** — проектная конкретизация признаков, по которым
зовут `av-dev:code-deep-review`, **двумя списками**: области, которые смотрят
целиком (узлы с частым возвратом, места с историей инцидентов, код под дорогое
решение), и **необратимое здесь** — что в этом проекте после мерджа не
откатывается обратной правкой. Второй список работает и в цикле задачи: находка
в таком месте уходит человеку развилкой, а не чинится молча. Перечнем мест,
узлами или capability, а не вторым определением класса;
- **Недоступно проверке** — два подраздела, оба **по темам**: «не проверит ни
один проход» (принципиальная граница, по факту промаха не пересматривается) и
«перестали проверять сознательно» (пересматривается первым). Тема, у которой в
проекте нет дома, сюда не пишется: её и так называет план каждого прогона.
**Журнал дефектов:** запись на каждый воспроизведённый дефект с пометкой
**проскочил / пойман ревью**. Запись — новое, и заводится она по слову человека
(`av-dev:doc-sync`, «Два рода правок»): «на каждый» задаёт **обязанность
предложить**, а не право записать молча. Человек отказал — записи нет, и
калибровка конвейера по этому дефекту не состоится; это его решение и его цена.
Проскочившие — проверочный набор для калибровки конвейера,
выборка по пометке. Пойманные с оракулом — лучшая опора для прохода: проектные,
воспроизводимые, однажды оказавшиеся правдой.
### `tasks/`
**Каталог задач канону не принадлежит.** Его ведёт скилл `task-track` — своим
скриптом и своими проверками; где каталог лежит и как названы его части, говорит
секция `[tasks]` в `.av-dev.toml`. Версия там одна на всю раскладку: канон и
каталог задач двигаются вместе, потому что ведёт их один плагин.
Каталог лежит **в корне репозитория, вне `docs/`** — места в `docs/` канон ему
больше не резервирует (переезд — запись 11 закрытого журнала), а внутрь не
смотрит и подавно: `docs.py` каталог не открывает, его отсутствия не считает
дрейфом и согласованность задач не проверяет. Проект, не заведший каталог задач, их не ведёт
вовсе, и отказом это быть не может.
Раскладку, форму записи и команды держит скилл `av-dev:task-track`. Ниже — то,
от чего зависит, читается ли проект как продукт.
**Индекс работ один — `BACKLOG.md`, и «что приложение уже умеет» он не
отвечает.** На этот вопрос отвечают `openspec/specs/` (нормативное поведение) и
`git log` индекса (когда и в каком порядке оно появилось). Роадмапа в каноне
нет: половину своего вопроса он дублировал беклогом, а вторую — спеками.
**У проекта есть стадия, и она решает, что значит порядок строк беклога:**
`build` — зависимость, `support` — важность. Канон её называет, потому что от
неё зависит, читается ли список работ как план стройки или как очередь правок;
механика — `task-track`, «Две стадии».
**У каждой задачи есть тип, и тип решает, что с ней можно делать.** Дом типа —
поле меты `Тип` первой строкой; эмодзи в заголовке от него производна. Словарь
закрыт:
| Тип | Что это |
| --- | --- |
| ✨ `feature` | снаружи появляется то, чего не было |
| 🐞 `fix` | поведение расходится с заявленным |
| 🧹 `chore` | обслуживание, поведение не меняется |
| 🔬 `research` | исход — знание, а не изменение |
**Схемы записи здесь нет намеренно.** Какие разделы тип требует — скилл
`av-dev:task-track`, раздел «Тип записи», подробно — по файлу на тип в его
`references/task-<тип>.md`. Канон фиксирует **словарь**, потому что от него
зависит, читается ли проект как продукт; схема — механика ведения задач, и второй
её экземпляр разошёлся бы с первым (он и разошёлся: канон успел объявить цель у
`fix` запрещённой, хотя она была необязательна, — и целей теперь нет вовсе).
Схема требуется **к взятию в работу**, а не к заведению: беклог пополняется чаще,
чем разбирается, и требование на входе выгоняло бы в заметки то, что должно
лежать задачей. Запись, не собравшая разделы своего типа, — законное состояние
беклога; невзятой её делает `tasks.py ready`.
Отдельного типа для незаполненной записи нет: «ещё не описано» — состояние, а не
род работы, и называется оно **`research` без раздела «Вопрос»**. Такая запись в
работу не берётся и лежит в конце своей категории.
Раскладку, форму записи и алгоритм работы над каждым типом держит скилл
`av-dev:task-track`.
### `CLAUDE.md`
Что это и стек; **инварианты с severity рядом с формулировкой** — по ним проходы
присваивают `critical`, поэтому severity стоит здесь, а не выводится каждым
проходом заново; команды; **семантика гейта** — чем краснеет безусловно и почему,
где логи, что означает исход, чего в гейте намеренно нет, **кто и когда обязан
гонять дорогое вне гейта**.
Плюс то, что нужно git-операциям и проходам и не выводится ниоткуда:
- **имя основной ветки** — от неё считается база диффа
(`git merge-base HEAD <ветка>`), в неё коммитит работу конвейер.
Угадывание между `master` и `main` ломает интеграцию целиком;
- **что запускать запрещено, с путями** — рабочая БД, боевой каталог данных,
внешние сервисы. Запретом с путями, а не «будь осторожен»;
- **где `testdata`** и что в них лежит; **куда писать временное**;
- **что считается необратимым** — единственный дом: от обратимости зависит вся
шкала ранжирования триажа и право проходов на `critical`;
- **что считается сломанным** — красная проверка, обгоняющая развитие;
**ориентир по размеру порции**, если он замерялся. Оба слота читает скилл
`av-dev:task-groom`, и имена их — его; названы они здесь потому, что дом
содержимого `CLAUDE.md` один и он тут.
### `openspec/config.yaml`
**Файл канону не принадлежит, и проверяет его тоже не канон.** Каталог
`openspec/` — предпосылка конвейера: без него не работают ни `opsx:propose`, ни
сверка требований. Заводит его, настраивает и **проверяет
форму** скилл `av-dev:code-openspec`: там образец файла, там же скрипт
`openspec.py check`. `docs.py` о файле не говорит ничего.
Канон называет его здесь по одной причине: `openspec/specs/` — **дом темы
`requirements`**, и без этой строки карта тем неполна. На форму самого
`config.yaml` канон не высказывается.
**Одно за каноном всё же остаётся, и это не форма, а единственный дом.** Блок
`context` — самое частое место для второго дома: он читается при порождении
каждого артефакта, туда удобно дописать «чтобы агент знал», и так заводятся копии
инвариантов, конвенций, состава гейта и правил ревью. Расходятся они молча.
Разрез: **утверждение, которое можно опровергнуть, открыв другой файл проекта, —
пересказ; строка, которая говорит, какой файл открыть, — ссылка.** Машина этого
не различает; судит агент `doc-consistency`, и `config.yaml` у него во входе.
## Правило единственного дома
Факт живёт ровно в одном файле; остальные ссылаются. Карта на случай спора:
<!-- дом: карта-домов -->
| Факт | Дом |
| --- | --- |
| поведение системы | `openspec/specs/<capability>/spec.md` |
| почему решено так | `adr/`, источник — архивный `design.md` либо записка разведки |
| граница домена, «чем не является» | `passport.md` |
| инвариант и его severity | `CLAUDE.md` |
| что осталось сделать и в каком порядке | `tasks/BACKLOG.md` |
| измеренное число | `research/` |
| настройка с числовым значением | `database.md` |
| периметр и модель угроз | `security.md` |
| что необратимо | `CLAUDE.md`**не** `architecture.md` |
| единые точки проекта | `architecture.md` |
| имя основной ветки, `testdata`, временный каталог | `CLAUDE.md` |
| что уже механизировано правилом | `conventions.*`, раздел «Механизировано» |
<!-- /дом: карта-домов -->
**Сколько чего в корпусе — тоже факт, и дом у него сам корпус.** «Пять ревью»,
«три capability», «четыре документа» в прозе — второй дом, расходящийся с первым
на ближайшем пополнении и молча. Правило и оба законных способа сослаться —
`av-dev/shared/language.md`, правило 10; здесь оно названо потому, что счёт
корпуса выглядит не копией, а собственным наблюдением документа.
## Пустое называется пустым
Скелет канона заводится **целиком** с первого дня. Незаполненный документ держит
**одну честную информативную строку**, а не заглушку:
- «внешних зависимостей нет — смотри на диск и на СУБД»;
- «наблюдений на живых данных нет: внешний источник один, формат документирован»;
- «прецедентов не накоплено»;
- «сознательно ничего не отключали»;
- «архитектуры пока нет: кода нет, заводится первой задачей».
Проход читает такую строку **как факт** и не тратит на неё обязательный вопрос.
Отсутствие файла он не может прочитать никак, а «TBD» читает как пробел —
поэтому `docs.py check` отличает честную строку от нетронутого плейсхолдера
шаблона и напоминает о втором.
## Слотов нет
Файлы и каталоги, которых в каноне **нет**, и куда уезжает их содержимое:
| Было | Куда |
| --- | --- |
| `docs/review-brief.md` | документы канона и есть бриф; остаток — в `review.md` |
| `docs/specs/` | `openspec/specs/` (поведение) и `architecture.md` (обзор) |
| `docs/drafts/` | идея → запись `research`; отказ → ADR; порядок → `BACKLOG.md`; размышление → `opsx:explore` |
| `docs/plan.md` | `tasks/BACKLOG.md` |
| `BRIEF.md` | `passport.md` |
| `docs/backlog/` | `tasks/` в корне репозитория |
| `docs/review-journal.md`, `docs/review/journal.md` | `docs/review.md` |
## Что проверяет машина, а что человек
Граница объявляется вслух в каждом отчёте: `check`, отчитавшийся «канон
соблюдён» на проекте, где из шести файлов три лишние, хуже отсутствующего.
| Проверяет `docs.py` | Судит агент | Какой |
| --- | --- | --- |
| отсутствующие пути канона | смысловой дубль документа и capability | `doc-consistency` |
| файлы в `docs/` вне канона | поведение, оставшееся в `architecture.md` | `doc-consistency` |
| имя файла не kebab-case латиницей; форма имени ADR | транслит в имени — сверх эвристики | `doc-wording` |
| битые относительные ссылки | прямое противоречие между документами | `doc-consistency` |
| версия канона и её отставание | достаточность честной строки в пустом слоте | `doc-consistency` |
| нетронутый плейсхолдер шаблона | ADR без ссылки на источник, замена без парного статуса | `doc-consistency` |
| маркеры долга — числом | **протухший факт, разошедшийся с кодом** | `doc-code-drift` |
| миграция изменена, а `database.md` нет | зависимость в манифесте, не названная в обзоре | `doc-code-drift` |
| capability без упоминания в `architecture.md` | второй способ там, где обзор обещал единственный | `doc-code-drift` |
| | **пересказ документа канона в `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` и `task-wording`.
**Зовутся оба одинаково и одним скиллом — `av-dev:doc-healthcheck`, на весь
канон разом; шагом `adopt` и шагом `upgrade` его зовёт `canon`.** Не на синке
документации: `doc-consistency` на
`opus` по каждой сделанной задаче не окупается, а расхождение между двумя документами по
определению требует двух, и на большинстве задач синк правит один. Пачка,
отбираемая работой, вдобавок не видит того, чего работа не касалась, — а именно
там расхождение и живёт: правка отменяет решение в одном документе, парный статус
нужен в другом.
**Перечень фактов, которые `doc-code-drift` сверяет с кодом, закрыт** — имя
основной ветки, команды, пути, зависимости поимённо, настройки с числовым
значением, единые точки проекта, capability, проверяемые инварианты. «Сверить
архитектуру с кодом» задача без дна, и агент, которому её поставили, выдаёт
правдоподобную труху вместо находок.
## `.av-dev.toml`
```toml
# Раскладка av-dev в этом проекте: версия и настройки проверок.
version = 1 # версия раскладки
[docs]
migrations = "internal/store/migrations" # если БД есть
healthcheck_last = "a1b2c3d" # сверка документов: коммит прошлого прогона
[tasks]
dir = "tasks" # каталог задач от корня репозитория
```
`version` — версия раскладки, под которую проект приведён, целым числом:
обратной совместимости нет, есть «приведён» и «не приведён». Число подставляет
`init`, `adopt` или `upgrade`, и берётся оно из `docs.py version`, а не из
образца: литерал в образце протухает на первом же повышении.
`[docs] migrations` — путь каталога миграций, если БД есть; по нему `docs.py`
делает сверку с `database.md`. `[tasks]` — где лежит каталог задач и как названы
его части; состав ключей описывает скилл `task-track`.
`[docs] healthcheck_last` — коммит, на котором в последний раз проходила сверка
документов; ставит его сам `av-dev:doc-healthcheck` последним шагом прогона, а
читает `av-dev:doc-sync`, чтобы сосчитать задачи, сделанные с тех пор. **Ключ
необязательный и в скелете его нет намеренно**: у нового проекта сверок не было,
и пустое значение врало бы про это меньше, чем отсутствие ключа, только на вид.
Отсутствие читается однозначно — «не сверялись ни разу».
Секции он достался по смыслу: `[docs]` — настройки проверок документов, а сверка
документов и есть такая проверка. Своя секция верхнего уровня стоила бы правки
общего читателя `shared/config.py` и сделала бы файл, объявленный «версией и
настройками», хранилищем состояния.
**Формат TOML взят ради комментариев.** Файл лежит в репозитории проекта, и
человек, открывший его через полгода, обязан прочитать в нём, что означает
число. JSON комментариев не знает, и объяснение приходилось держать в другом
файле. Отсюда же правило записи: скрипты правят **строку**, а не переписывают
файл — перезапись стёрла бы то, ради чего формат и выбран.
**Файл один, и лежит он в корне.** До слияния плагинов их было два —
`docs/.docs.json` с версией канона и `<каталог задач>/.tasks.json` с версией
формата задач, — и версии двигались порознь, потому что плагины ставились
порознь. Плагин теперь один, версия одна, а корень выбран потому, что он есть и
у проекта без `docs/`, и у проекта без каталога задач. Прежние имена не
читаются: два дома для одной версии расходятся молча. Увидев их, `docs.py` и
`tasks.py` говорят «прежняя раскладка» и зовут `upgrade` — версия 1 журнала.
Ключей будет больше по мере роста проверок, но **заводятся они только вместе с
правкой скрипта**: неизвестный ключ — не безмолвный пропуск, а **отказ кодом
3**. Верхний уровень стережёт `shared/config.py` (`TOP_KEYS`), секцию `[docs]`
`docs.py` (`DOCS_KEYS`), секцию `[tasks]``tasks.py`. Довод у отказа
проверяемый: ключ, положенный не в ту секцию, при молчаливом пропуске не значит
ничего — проверка объявляет себя неприменимой, отчёт выходит зелёным, и на месте
настройки оказывается тишина.
**Здесь это правило однажды соврало, и цена была немедленной.** Абзац обещал, что
неизвестный ключ игнорируется; по этому обещанию скилл сверки завёл себе секцию
`[healthcheck]` верхнего уровня — и первый же её прогон сделал бы нерабочими
`docs.py`, `tasks.py` и гейт проекта, который их зовёт. Отсюда и порядок: **новый
ключ заводится правкой константы в скрипте-владельце, и только потом появляется
здесь**.
**Отсутствующий** ключ — другое дело: он значит «проверка неприменима», и скрипт
говорит об этом строкой, а не молчит.
@@ -0,0 +1,784 @@
# Журнал версий канона до слияния плагинов
**Журнал закрыт.** Он описывает версии канона документов 1–14 — время, когда
плагинов было три и у канона была своя нумерация. Действующий журнал —
[changelog.md](changelog.md), и его версия 1 идёт **после** записи 14 отсюда.
Записи не переписаны под нынешние имена: адрес и имя скилла, верные на день
записи, остаются там как свидетельство. Записи ниже версии 13 зовут служебный
файл `docs/.pm.json` — так и было; переименование делает запись 13, а переезд в
`.av-dev.toml` — запись 1 действующего журнала.
Проект, отставший от канона 14, идёт сперва по этим записям снизу вверх от своей
версии до 14, и только потом переходит в действующий журнал.
---
## Версия 14 — 2026-08-11
У ADR стало два законных источника. Прежде запись цитировала только архивный
`design.md`, то есть решение, принятое по ходу изменения. Решение, принятое
**разведкой** — намеренный отказ, выбор подхода, «проверили и не делаем», — не
имеет `design.md` по построению: change по нему не заводится никогда. Триггер
канона такое решение ловит («намеренный отказ от очевидного подхода»), а дома у
него не было, и оно оседало в записке разведки или в переписке.
**Что изменилось.** `adr/` принимает второй источник — записку разведки. Правило
«промоут, а не второе сочинение» не тронуто: запись по-прежнему цитирует уже
написанное и **называет источник**, изменилось только то, что источников два.
Следом сказали то же самое: карта домов, разрез проверки `doc-consistency`, вход
и устав самого агента, скелеты `docs/adr/README.md` и `docs/adr/template.md`.
**Почему это версия, а не правка текста.** Два следствия уезжают в репозиторий
проекта. По карте домов судит агент согласованности — прежняя редакция читала ADR
со ссылкой на записку разведки как нарушение; а скелеты `adr/` лежат в проекте
файлами и говорят там от имени канона.
**Что сделать проекту.**
1. Ничего с существующими записями: прежние ADR ссылаются на `design.md`, и это
по-прежнему верно.
2. **Поднять шапку `docs/adr/README.md`**: «промоут поверх архивного `design.md`»
→ «промоут поверх уже написанного», с обоими источниками. Точный текст — в
[skeletons.md](skeletons.md), раздел `docs/adr/README.md`.
3. **Поднять `docs/adr/template.md`**: строка `- **Источник:**` называет два
возможных источника.
4. `docs/.docs.json`: `"canon": 14`.
**Чего делать не надо.** Заводить ADR задним числом по старым разведкам. Запись
заводится, когда решение принимается, а не когда о нём вспомнили: сочинённое
через полгода обоснование — ровно то «второе сочинение», против которого правило
и написано.
---
## Версия 13 — 2026-08-11
Служебный файл канона переименован: `docs/.pm.json``docs/.docs.json`. Имя
досталось от плагина `av-dev-pm`, который распался на четыре и которого больше
нет: файл пережил владельца и указывал в пустоту. Правило простое и теперь
соблюдается всеми тремя: **имя служебного файла — имя плагина, который его
завёл**, `.docs.json` — канон, `.tasks.json` — задачи, `openspec/config.yaml`
конвейер.
**Что изменилось.** `docs.py` читает только новое имя. Прежнее он не читает
намеренно: два дома для одной версии канона расходятся молча, а тут расхождение
стоило бы дорого — по этому числу `upgrade` решает, какие записи применять.
Файл под старым именем `check` узнаёт и называет отдельной строкой с готовой
командой, а не жалуется на пропажу.
**Что появилось у соседа.** У каталога задач теперь есть **своя версия
формата** — ключ `tasks` в `<каталог задач>/.tasks.json`, — и свой журнал версий
в скилле `av-dev-tasks:tasks`. До сих пор её не было вовсе: формат задач менялся
записями этого журнала (8, 11, 12), хотя каталог принадлежит другому плагину и
ставится без канона документов. Канон это число не двигает.
**Что сделать проекту.**
1. `git mv docs/.pm.json docs/.docs.json` — одним коммитом с шагом 2. Содержимое
не меняется: ключи те же.
2. **Поправить упоминания прежнего имени** в своих файлах — `CLAUDE.md`, гейт,
`README.md`, `docs/**`. Битой ссылкой это чаще всего не выглядит (файл
служебный, на него ссылаются прозой), поэтому `docs.py check` таких упоминаний
не ловит: ищи `grep -rn '\.pm\.json'` по репозиторию.
3. **Объявить версию формата задач**, если каталог задач в проекте есть:
`<каталог задач>/.tasks.json` с ключом `"tasks": <версия>`. Файла нет вовсе —
заведи, он теперь обязателен: версия не настройка, от которой можно
отказаться. Какое число ставить и что сделать перед этим, говорит журнал
владельца — **позови скилл `av-dev-tasks:tasks`**, здесь этих шагов нет
намеренно: второй перечень чужих шагов разошёлся бы с первым.
4. Гейт не меняется: шаги те же, версию задач сторожит `tasks.py check`, который
в нём уже стоит.
5. `docs/.docs.json`: `"canon": 13`.
**Чего делать не надо.** Ключи в файле не трогаются, документы не переезжают,
записи задач не меняются: версия 13 — про имена служебных файлов и про то, кто
чью версию двигает.
---
## Версия 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/` был предпосылкой, о которой канон говорил, но за которой не следил.
Каталог назван в раскладке, `openspec/specs/` объявлен домом темы `requirements`,
`config.yaml` описан абзацем — а заводил всё это человек руками, и проверялось
из перечисленного ничего. Заведение нового проекта проходило мимо: `init`
собирал документы канона и оставлял проект без каталога, без которого не работают
ни `opsx:propose`, ни сверка требований.
Хуже отсутствия оказался файл из коробки. `openspec init` кладёт `config.yaml`,
где `context` и `rules` — закомментированный пример на английском. Такой файл
читается как настроенный: он есть, он валиден, имя правильное. Работает он как
пустой, и узнаётся это по предложению, написанному на другом языке, с
capability по имени пакета и без единого `SHALL`.
**Что изменилось:**
1. **`init` заводит OpenSpec сам** — `openspec init --tools claude`, до первого
документа канона. Команда названа в каноне поимённо, потому что её печатает
отказ `docs.py`.
2. **У `openspec/config.yaml` появилась каноническая форма** и скелет в
`skeletons.md`. Содержание — только то, что нужно **в момент порождения
артефакта**: язык, правила именования capability, придирки валидатора и
**адреса** документов канона. Пересказ паспорта, инвариантов, конвенций и
правил ревью в него не переносится.
3. **`docs.py check` проверяет пять вещей:** каталог `openspec/` есть; файл
называется `config.yaml` (`config.yml` OpenSpec читать не станет и об этом не
сообщит); `context` и `rules.specs` не остались примером, а правила для
`specs` называют `SHALL`; `context` называет `passport` и `CLAUDE.md`; ключи
под `rules:` — имена артефактов схемы, а не опечатки.
4. **За свежестью формы следит машина, а не память.** Схема и перечень
артефактов — слепок чужого инструмента; `check` сравнивает `major.minor`
установленного OpenSpec с версией, на которой форма сверялась, и при
расхождении даёт замечание. Перепроверяет `docs.py openspec-form`, и чинится
расхождение **в плагине, а не в проекте**.
5. **Шестое проверяет агент.** Отличить ссылку на документ от пересказа документа
машина не умеет — это работа `doc-consistency`, и в таблице «Что проверяет
машина, а что человек» она стоит строкой.
**Что переехало:** ничего в раскладке `docs/`. Ни один файл не переименовывается
и не перемещается.
**Что сделать проекту:**
1. Нет `openspec/` — завести: `openspec init --tools claude`. Команда кладёт ещё
и `.claude/skills/openspec-*` с `.claude/commands/opsx/*`; это её нормальная
работа, удалять их не надо.
2. Открыть `openspec/config.yaml` и привести к скелету из
[skeletons.md](skeletons.md): блок `context` с языком, правилами именования
capability, требованием `SHALL` и **адресами** `docs/passport.md` и
`CLAUDE.md`; блок `rules` с четырьмя правилами для `specs`.
3. **Вычистить из `context` пересказ.** Инварианты, перечень конвенций, состав
шагов гейта, правило выбора метки и состав проходов ревью — заменить ссылкой
на дом. Признак пересказа простой: строку можно опровергнуть, открыв другой
файл проекта.
4. Проверить имя файла: `config.yml` переименовать в `config.yaml`. Если жили оба
— содержимое `.yml` до сих пор не читалось никем, и переносить из него нужно
именно то, чего нет в `.yaml`.
5. `docs/.pm.json`: `"canon": 7`.
---
## Версия 6 — 2026-08-07
Версия 5 объявила: **каждый документ `docs/` — тема ревью**. Правило оказалось
верным ровно наполовину и потому вредным целиком. Паспорт и схему хранилища
ревью читает, но темами они не являются — они задают границу, по которой судит
чужая тема. Журнал решений и журнал наблюдений ревью изменения не нужны вовсе:
ADR объясняет прошлое решение, а не предъявляет требование к изменению.
Разметчик, применявший плоское правило буквально, обязан был либо завести
фантомные темы `passport`, `adr`, `database`, `research` и продублировать ими
работу тем `architecture` и `operations`, либо потерять четыре документа молча —
а молчащая потеря и есть то, против чего канон написан.
**Что изменилось:**
1. **Три категории документов вместо одной.** Разрез проверяемый: можно ли по
документу сказать «в этом изменении сделано не так»? **Тема** — да, прямо
(`conventions`, `security`, `architecture`, свои документы проекта).
**Источник темы** — нет, но он задаёт границу для чужой темы (`passport.*`
`architecture`, `database.*``operations`, `CLAUDE.md``autotests`,
`openspec/specs/``requirements`). **Процессный документ** — нет, он про то,
как мы работаем (`tasks/`, `review.*`, `adr.*`, `research.*`, `.pm.json`).
2. **Категории `источник` и `процессный` закрыты, категория `тема` открыта.**
Прежде открытым был весь список, и «не темы ровно две» противоречило
собственной раскладке канона. Теперь пополняется только одно множество, и
документ, которого нет в раскладке, — однозначно своя тема проекта.
3. **`adr/` и `research/` уходят из входа ревью изменения.** Прогон их больше не
открывает. Проверяться они не перестали: ADR без ссылки на архивный
`design.md`, замена без парного статуса, число без провенанса — это по-прежнему
работа `doc-consistency` и `doc-code-drift`, на сессии между спринтами.
4. **`docs.py` печатает категорию в отказе.** «Нет источника passport» читается
иначе, чем «нет темы security». Обязательность при этом не изменилась:
заводятся все документы одинаково и с первого дня.
5. **У задачи появилась метка — `small`, `medium` или `large`.** Это итог
классификации и **единственный вход, по которому конвейер выбирает
исполнителей** на обеих стадиях ревью. Прежние имена `quick`, `standard` и
`wide` описывали глубину прогона, то есть свойство ревью; метка описывает
**задачу** — а выбирают по ней одно и то же. Слово «ступень» уходит:
у одной вещи одно имя.
6. **Метка выводится из двух осей и не равна ни одной из них.** Размер (малое,
среднее, крупное) и сложность (знакомое, незнакомое); метка — максимум по
ним. Малое **незнакомое** изменение получает `large`, трогая один узел, —
поэтому размер и метка пишутся отдельными строками, и выводить одно из
другого нельзя.
**Цена, записанная явно:** расхождение изменения с записанным решением прогоном
больше не ловится. Раньше архитектурный проход мог сказать «здесь отменено
решение ADR-2026-03-11, парного статуса нет»; теперь это скажет только сверка
документации. Сделка сознательная: чтение всего каталога решений оплачивалось на
каждой задаче, а срабатывало на единицах.
**Что переехало:** ничего в раскладке. Ни один файл не переименовывается и не
перемещается.
**Что сделать проекту:**
1. `docs/review.*`, подраздел «Вопросы по темам»: убрать вопросы, адресованные
`passport`, `database`, `adr`, `research` и `review` — **ни одно из этих имён
больше не тема**. Под каноном 5 темой был каждый документ `docs/`, поэтому
такие вопросы там законны и почти наверняка есть. Переадресовать:
про границу домена и про решение → `architecture`; про хранилище, настройку и
измеренное число → `operations`. Вопрос, который никуда не переадресовывается,
удалить, а не оставить висеть: адресованный несуществующей теме, он не
задаётся никем и молча.
2. Там же, «Недоступно проверке»: те же пять имён убрать из разнесения по темам,
переразнеся содержимое по оставшимся.
3. Там же: подраздел **«Триггеры профиля» → «Триггеры метки»**, и разнести его
на **три** списка вместо двух — «крупное здесь» (про объём), «незнакомое
здесь» (про форму решения) и «мелкое здесь» (опускает до `small`). Раньше
первые две оси были склеены в один список, и потому объём в правило по факту
не входил.
4. **Переименовать метки прогона везде, где проект их называет** — в «Триггерах
метки», в «Недоступно проверке», в журнале дефектов: `quick`**`small`**,
`standard`**`medium`**, `wide`**`large`**. Метка это итог классификации
задачи, и три её значения — часть общего словаря канона и конвейера. Слово
«ступень» из документов уходит: у одной вещи одно имя.
5. Проверить, что свои темы проекта не совпадают именем с закрытыми категориями:
`docs/passport/`, `docs/adr/`, `docs/research/`, `docs/database/`,
`docs/review/` — это слоты канона, а не свои темы, и своим смыслом их
наполнять нельзя.
6. Ничего не заводить и не удалять: раскладка канона 6 совпадает с раскладкой
канона 5 файл в файл.
7. `docs/.pm.json`: `"canon": 6`.
---
## Версия 5 — 2026-08-06
Канон перестал быть списком файлов и стал **списком тем ревью**. Раскладка та же,
но читается иначе: документ в `docs/` — это направление проверки, а не просто
текст. Отсюда три правки, и все три развязывают то, что раньше было жёстко
сцеплено.
**Что изменилось:**
1. **Тема живёт файлом или каталогом, на выбор проекта.** `docs/security.md` и
`docs/security/` — одно и то же; тема разрослась, стала каталогом с
`README.md` — канон не сменился и версия не двинулась. Прежде форма была
задана поимённо: `conventions`, `research` и `adr` обязаны были быть
каталогами, остальные — файлами, и обосновать это было нечем. Обе формы сразу
— ошибка: два дома для одного факта расходятся молча.
2. **Список тем открытый.** Всё, что проект кладёт в `docs/`, становится темой
ревью и попадает в план каждого прогона; именной оптики у такой темы нет, её
разбирает общий проход конвейера, заведённый ровно за этим.
Прежде `docs.py` называл незнакомый файл «вне канона» — теперь называет своей
темой проекта и перечисляет их в отчёте. Не темы ровно две: `docs/tasks/` и
`docs/review.*`.
3. **`AGENTS.md` рядом с `CLAUDE.md` — законно.** Он почти стандарт; обязателен
по-прежнему только `CLAUDE.md`, но если лежат оба, читаются оба, и проверки
канона смотрят на второй так же, как на первый.
**Что переехало:**
- в `docs/review.*`: **«Вопросы к проходам» → «Вопросы по темам»**, форма
`<тема>: <вопрос> (<провенанс>)`. Причина не косметическая: вопрос,
адресованный проходу, перестал задаваться молча в тот день, когда тот уехал в
верхнюю ступень ревью. Тема переезд прохода переживает, имя прохода — нет;
- там же **«Недоступно проверке» — по темам**, оба подраздела.
**Что сделать проекту:**
1. Ничего не переименовывать, если всё уже разложено по канону 4: обе формы
дома законны, и текущая — одна из них.
2. `docs/review.*`, подраздел «Вопросы к проходам»: переименовать в «Вопросы по
темам» и переадресовать каждый вопрос теме вместо имени прохода. Темы ядра —
`requirements`, `autotests`, `conventions`, `architecture`, `security`,
`operations`.
3. Там же «Недоступно проверке»: разнести обе половины по темам.
4. Проверить, не лежит ли в `docs/` документ, который раньше считался лишним и
потому не заводился. Теперь он законен и станет темой ревью — это и есть
способ добавить проверку, которой в конвейере нет.
5. `docs/.pm.json`: `"canon": 5`.
6. Позвать судей `doc-consistency` и `doc-code-drift` — шагом 6 `upgrade`.
## Версия 4 — 2026-08-05
Две правки, обе про то, как читается каталог задач. Первая — секция роадмапа
переименована, и вместе с именем расширен её смысл; достигнутое переехало вниз.
Вторая — **у каждой записи появился тип, и тип определяет, что с записью можно
делать**. Раскладка не меняется, файлов канона не прибавляется.
**Что переехало:**
- секция роадмапа `Разработка`**`Сопровождение`** (англ. `Tooling`
**`Operations`**). Прежнее имя называло слишком много: роадмап **весь** про
разработку, и секция с таким именем не отличалась от остальных ничем;
- **тип записи** — из префикса заголовка (`[goal]`/`[idea]`) и тега
`kind:<род>` в **поле меты `Тип`** первой строкой. Эмодзи в заголовке от него
производна;
- **поле места** у задачи: `Секция`**`Категория`**. У цели остаётся `Секция`:
у задачи поле называет полку домена, в которую она вернётся из спринта, у цели
— часть роадмапа, то есть состояние очереди. Одно имя на два смысла их и
смешивало.
**Что добавилось:**
1. **Смысл секции расширен.** Было «инструмент и процесс», стало «чем держат
проект: инструмент, процесс, эксплуатация». Метрики, логи, инфраструктура,
выкладка и дежурство — сюда же. Расширение не косметическое: английское
`Operations` при узком смысле обещало бы эксплуатацию, а внутри лежал бы
линтер.
2. **Общий словарь трёх мест** — [canon.md](canon.md), раздел «Сопровождение и
эксплуатация». Сопровождение — всё, чем держат проект; эксплуатация — его
часть, работа системы на проде. `ROADMAP.md`, секция `Сопровождение` — план
работ; `architecture.md`, раздел «Эксплуатация» — как устроено сейчас;
эксплуатационный проход ревью — оптика проверки. Слить их в одно слово
нельзя: они отвечают на разные вопросы. Слово **«поддержка» не употребляется
вовсе** — в нём слышится помощь пользователю.
3. **Граница с возможностями проходит по тому, кто наблюдает.** «Приложение
сообщает о своём состоянии» — возможность приложения, её место среди прочих
целей. «Дежурный видит состояние на одном экране» — сопровождение. Одни и те
же метрики попадают в разные секции роадмапа, и это верно.
4. **Порядок секций стал каноническим**, и `Готово` переехало **вниз**:
`Запланировано` | `Направления` | `Сопровождение` | `Готово`. Достигнутое
копится — через год этой секции больше, чем всех остальных вместе, — и стоя
первой она отодвигает за экран то, ради чего роадмап открывают чаще всего.
Порядок проверяет `tasks.py check`, переставляет `check --fix`.
5. **Заголовок секции отбивается пустой строкой с обеих сторон.** Прежде
проверялась только строка после заголовка; перестановка секций двигает целые
блоки, и два заголовка оказываются вплотную. Правит `check --fix`.
6. **Тип — единственная ось записи, закрытый словарь из пяти значений:**
`goal` | `feature` | `fix` | `chore` | `research`. Осей было две — тип записи
(`goal`/`idea`/`task`) и род работы (`kind:` тегом), — но из двенадцати
клеток произведения законны были шесть, а алгоритм работы крепится к роду, а
не к типу. Оси схлопнуты.
7. **Тип задаёт схему тела:** какие разделы обязательны, какие допустимы, нужна
ли цель, берётся ли запись в спринт. Проверяет `sprint take`, замечания даёт
`check`. Два раздела новые: **`Воспроизведение`** у `fix` (не
воспроизводится — это `research`, а не `fix`; правило было записано и не
проверялось) и **`Вопрос` + `Куда ляжет ответ`** у `research` вместо
критериев приёмки (приёмка разведки — записанный ответ, и критерии в форме
«оракул: тест» ей натянуты).
8. **Тип `idea` упразднён.** Он значил не род работы, а состояние
незаполненности, а состояние типом быть не может. Теперь оно называется
честно: `research` без раздела «Вопрос» — **сырьё**. В спринт не берётся, как
и прежняя идея, лежит **в конце своей категории** (проверяет `check`,
переставляет `--fix`) и отбирается `list --raw`. Порядка «по важности» в
беклоге по-прежнему нет: этот порядок производен от типа, а не назначен
человеком.
9. **Алгоритм работы над каждым типом** — отдельным файлом,
`skills/tasks/references/task-<тип>.md`: схема, что проверяет машина, что
человек, и порядок шагов.
10. **Имена файлов проверяются.** Правило «текст русский, имена английские»
стояло в каноне и не было подкреплено ничем: `docs.py` имён не смотрел вовсе.
Теперь смотрит — кириллица и не-kebab-case **жёстко**, форма имени
`ADR-ГГГГ-ММ-ДД-slug.md` жёстко, транслит **эвристикой**, то есть
замечанием. Заодно из раскладки канона убраны плейсхолдеры `<тема>.md`,
приглашавшие называть файлы по-русски.
11. **Два агента вместо обещания.** В каноне была таблица «Что проверяет машина,
а что человек», и её правая колонка три версии описывала судью, которого не
существовало. Судьи заведены и разведены по глубине: **`doc-consistency`**
(документ ↔ документ ↔ openspec: факт в двух домах, прямое противоречие,
поведение в `architecture.md` вместо спек, ADR без ссылки и парного статуса,
число без провенанса, заглушка вместо честной строки); **`doc-code-drift`**
(документ ↔ код по закрытому перечню фактов). Оба зовутся раз в спринт на
сессии, а также после adopt и после upgrade, на весь канон разом.
**Что сделать проекту:**
1. Переименовать заголовок секции в `docs/tasks/ROADMAP.md`: `## Разработка`
`## Сопровождение` (или `## Tooling``## Operations`, если индекс
английский). **`check --fix` этого не сделает**: регистр канонической секции
он правит сам, а чужую секцию только называет ошибкой — смысл за человеком.
2. Поправить поле `- **Секция:**` в файлах целей, которые в ней лежат. Порядок
именно такой: сперва заголовок, потом `python3 tasks.py check --dir
docs/tasks` покажет расхождение поимённо.
3. Перечитать состав секции: цели про выкладку, метрики, логи и инфраструктуру,
если они лежали в `Направлениях` за неимением места, переезжают сюда.
4. Прогнать `python3 tasks.py check --dir docs/tasks --fix`. За один проход он
переставит секции роадмапа в канонический порядок (`Готово` уедет вниз вместе
со всем содержимым), поправит отбивку заголовков и **переведёт записи на
типы**: перенесёт значение из тега `kind:` и префикса `[goal]`/`[idea]` в поле
`Тип`, снимет тег, поставит эмодзи в заголовок, переименует `Секция`
`Категория` у задач и снесёт сырьё в конец категорий.
5. Разобрать то, что `--fix` вернул пометкой `НЕОДНОЗНАЧНО`. Главный случай —
**записи без типа**: заведённые до появления рода работы, они не несут ни
тега, ни префикса, и машина их не угадывает (`feature` от `chore` не
отличает). Проставить руками: `edit <слаг> --type …`.
6. Дописать новые обязательные разделы у задач, которые собираются в спринт:
`Воспроизведение` у каждого `fix`, `Вопрос` и `Куда ляжет ответ` у каждого
`research`. Не «заодно по всему беклогу», а порциями переоценки: `check`
ошибкой это не считает, отказывает только `sprint take`. Сколько задач готово
к взятию, печатает блок здоровья `check`.
7. Прогнать `python3 docs.py check`: он назовёт имена файлов не по правилу.
Кириллицу и не-kebab-case править обязательно, транслит — по решению
человека. **Переименование ADR это перенос ссылок**: слаг стоит в
`adr/README.md`, в `architecture.md` и в чужих документах, и делается одним
проходом, иначе останутся битые ссылки (их `docs.py` потом и покажет).
8. `docs/review.md`, подраздел «Триггеры профиля» — переписать целиком, он
отстал дважды. Снести перечень мест для `deep`: профиль упразднён вместе с
проходом независимой реализации, и перечень стал указателем в пустоту.
Оставшийся перечень перевести на новое правило: `wide` теперь означает не
«новое понятие», а **крупное или незнакомое** изменение и рассчитан на 5–10%
задач; отдельным списком назвать, что здесь считается **мелким** (это `quick`).
Форма подраздела — в [skeletons.md](skeletons.md). Там же проверить журнал
дефектов и «Недоступно проверке» на упоминания независимой реализации: класс
«форма решения, где спека выбора не сделала» переезжает в подраздел «перестали
проверять сознательно», а рядом с ним встаёт вторая честная строка — на
`quick` и `standard` не проверяется ничего, что требует запуска.
9. `docs/.pm.json`: `"canon": 4`.
10. Позвать **обоих судей**`doc-consistency` и `doc-code-drift`, шагом 6
`upgrade`. Пунктов выше десять, половина из них ручная, и именно здесь видно,
какие сделаны только наполовину: переименования секций и полей разводят
документы, а `check` сверяет число версии, а не существо. Первый прогон на
живом проекте вдобавок самый урожайный — правило единственного дома до сих пор
никто не проверял. Разбирать порциями, а не одним заходом.
## Версия 3 — 2026-08-04
Роадмап стал **состоянием проекта**, а не очередью работ: цель — возможность
приложения, задача — шаг к ней, достигнутое из роадмапа не исчезает. Плюс род
работы, раздел «Затрагивает» и новое умолчание профиля ревью. Раскладка меняется
в одном файле, но переименование и смена секций тянут за собой ссылки, поэтому
шаги делаются одним заходом.
**Что добавилось:**
1. **Род работы** — тег `kind:<род>` в мете задачи, словарь закрыт:
`feature` | `fix` | `chore` | `research`. Обязателен у задачи, у цели
запрещён. `sprint take` без него отказывает, `check` о пропаже напоминает
замечанием. Определение — [canon.md](canon.md), раздел `tasks/`; смысл и
причина, почему тегом, — в SKILL.md скилла `tasks`, раздел «Род работы».
2. **Раздел «Затрагивает»** в теле задачи — перечень границ, которых изменение
касается (эндпоинт, таблица и миграция, формат на диске, публичный тип). Как
и критерии приёмки, требуется к взятию в спринт, а не к заведению.
3. **Секции роадмапа** — четыре вместо двух и **канонические**, в отличие от
секций беклога: `Готово` (достигнутые цели строкой с датой, без ссылки на
файл), `Запланировано` (очередь значима), `Направления` (очереди нет),
`Разработка` (инструмент и процесс, не возможности приложения). Английский
вариант — `Done` | `Planned` | `Directions` | `Tooling`, один язык на весь
индекс. Переименованию проектом не подлежат: у каждой свой смысл, и в первую
пишет сам `close`; `tasks.py check` проверяет состав.
4. **Форма заголовка записи** — по типу: задача отвечает на «что нужно сделать»
и пишется глаголом в неопределённой форме («Не отбрасывать молча лишние
символы»), цель — на «что приложение будет уметь», идея просто называет, о
чём она. `check` считает заголовки не в форме действия и печатает число в
блоке здоровья. Годность формулировки — не машине: её смотрит новый агент
`task-form` (форма записи, только чтение), а язык текста — `doc-wording`.
5. **Заголовки секций — с прописной, после заголовка пустая строка**, во всех
индексах. Написание канонических секций и отбивку правит `check --fix`; он
же сводит написание секции в мете файла с заголовком индекса.
6. **Язык проектных текстов** — [language.md](../../../shared/language.md), общий дом для
документов канона, задач, решений ADR и записок разведки: информационный
стиль (глагол вместо отглагольного существительного, активный залог, факт
вместо оценки, стоп-слова, параллельность), таблицы англицизмов и жаргона и
то, что из стиля отброшено намеренно. Проектных файлов не добавляет и
раскладку не меняет — это правила письма, а не новый слот.
7. **Умолчание профиля ревью сменилось** — это не раскладка, но проектный текст
под него уже написан. `standard` стал рабочим умолчанием: миграция схемы,
публичный контракт и инвариант ступень больше **не** поднимают, `wide`
означает новое понятие или структурную единицу. Подраздел «Триггеры профиля»
в `docs/review.md` остаётся на месте, но его содержимое надо перечитать.
**Что переехало:** `docs/tasks/PLAN.md``docs/tasks/ROADMAP.md`; достигнутая
цель — из небытия в секцию `Готово`: `close <цель> --implemented` удаляет файл, но
**оставляет строку с датой**. Прежде роадмап отвечал только «что осталось», и
половину его вопроса вели прозой руками. Вместе с
файлом переименован ключ конфига `tasks.plan``tasks.roadmap` и токены
команд: `--index plan``--index roadmap`, `init --plan-sections`
`--roadmap-sections`, `init --plan``--roadmap`. Старый ключ в
`docs/.pm.json` не игнорируется молча — `tasks.py` останавливается и называет
переименование.
**Что удалено:** тип `[epic]`. Он был зонтиком между целью и задачами; зонтиком
стала цель, а слишком крупный шаг дробится на шаги помельче под ней. Ноль
употреблений на 97 записей двух живых проектов.
**Что сделать проекту:**
1. `git mv docs/tasks/PLAN.md docs/tasks/ROADMAP.md`.
2. Починить ссылки на прежнее имя: `grep -rn 'PLAN\.md' docs/ CLAUDE.md`
заголовок самого файла («# План» → «# Роадмап»), строка в `docs/tasks/BACKLOG.md`,
упоминания в `docs/passport.md` и в телах задач.
3. `docs/.pm.json`: ключ `tasks.plan`, если он там был, — в `tasks.roadmap`.
4. Проставить род работы живым задачам: `python3 tasks.py check --dir docs/tasks`
перечислит те, у кого его нет. Задним числом весь беклог не переоформляется —
род нужен к взятию, так что порядок такой: сперва то, что берётся в ближайший
спринт, остальное по ходу переоценки.
5. Дописать раздел «Затрагивает» — тем же порядком и по той же причине: сперва
набор спринта, остальное по мере того, как задача попадает в работу.
6. Перечитать «Триггеры профиля» в `docs/review.md`: строки вида «миграция →
`deep`» теперь дублируют умолчание с обратным знаком. Оставить там только то,
что для этого проекта считается **новым понятием** и **правилом
идентичности**, — и убрать остальное, иначе проект возвращает себе прежнюю
частоту полного набора уточнением.
7. Переименовать секции роадмапа: `порядок``Запланировано`, `темы`
`Направления`; завести `Готово` **первой** и `Разработка` последней
(порядок секций поменялся в версии 4 — если едешь сразу на неё, заводи
`Готово` последней и не переставляй дважды).
Прозаические разделы вроде «Что уже пройдено», которые велись руками,
разложить: звенья — строками в `Готово` (дата, слаг, что стало возможно),
обоснование очереди оставить прозой в `Запланировано`. Любой `##` в индексе
проверка считает секцией, и теперь `check` называет чужую секцию ошибкой.
8. Переформулировать цели ответом на **«что приложение будет уметь»**: не
«Работа со слиянием», а «Исход слияния не зависит от порядка доставки».
Свойство поведения — законная цель. Цель, которая не про приложение
(процесс, инструмент), переезжает в `Разработка`.
9. `[epic]`, если он в проекте заводился: это либо цель, либо набор задач под
общей целью. `check` назовёт его неизвестным типом.
10. Прогнать `python3 tasks.py check --dir docs/tasks --fix`: он поднимет
написание канонических секций, поставит отбивку после заголовков и сведёт
секцию в мете файлов с заголовками индексов. Секции беклога проект
переименовывает сам — их имена он выбирал, и трогать их скрипт не вправе.
11. Переписать заголовки задач в форму действия — по мере того, как задача
попадает в работу, а не «заодно»: `check` печатает их число, а `task-form`
предложит формулировки на замену пачкой.
12. Прочитать [language.md](../../../shared/language.md) — и **ничего не переписывать задним
числом**. Правила языка применяются к тому, что пишется и правится сейчас;
сплошная вычитка старых документов стоит дороже, чем даёт.
13. `docs/.pm.json`: `"canon": 3`.
## Версия 2 — 2026-08-03
Шапка записи ADR — мета-блоком общей формы, и у статуса появился объявленный
дом. Раскладка не менялась: правка касается одного шаблона.
**Что добавилось:** поле `- **Статус:**` в шапке `docs/adr/template.md`
`заменено на ADR-…` либо `устарело`, у активной записи поля нет. Правило
«старая запись получает статус» было и раньше ([canon.md](canon.md), `adr/`),
но места под него шаблон не отводил: каждая запись изобретала своё, а колонка
«Статус» таблицы `adr/README.md` брала его оттуда, где он у каждого свой.
**Что переехало:** поля `Дата` и `Источник` в шаблоне стали жирными
(`- **Дата:**`, `- **Источник:**`) — та же форма, что у меты задачи и у записи
журнала дефектов: поле на строку, имя жирным.
**Что удалено:** ничего.
**Что сделать проекту:**
1. Привести `docs/adr/template.md` к скелету версии 2
([skeletons.md](skeletons.md), раздел `docs/adr/template.md`).
2. В существующих записях `docs/adr/ADR-*.md`: жирным поля шапки; если статус
записан прозой или заголовком — перенести его полем `- **Статус:**` в шапку
и сверить с колонкой «Статус» таблицы в `docs/adr/README.md`.
3. `docs/.pm.json`: `"canon": 2`.
## Версия 1 — 2026-08-03
Первая версия. Проект любой прежней раскладки приводится к ней скиллом `canon`
в режиме `adopt`, а не `upgrade`.
**Что вводится:** раскладка целиком — см. [canon.md](canon.md).
**Что сделать проекту, который приходит из свободной раскладки:**
1. `docs/.pm.json` с `{"canon": 1}` и путём миграций, если БД есть.
2. Скелет канона целиком; незаполненное — одной честной строкой.
3. `docs/specs/` разобрать: поведение — в `openspec/specs/`, обзор — в
`docs/architecture.md`, знание о чужих системах — в `docs/research/`.
Дубли capability удалить, сверив поимённо.
4. `docs/plan.md``docs/tasks/PLAN.md`, шаги плана — целями в «порядок».
5. `BRIEF.md``docs/passport.md`.
6. `docs/backlog/``docs/tasks/`.
7. `docs/review-journal.md` или `docs/review/journal.md``docs/review.md`,
плюс раздел настройки конвейера.
8. `docs/drafts/` растворить: идея → задача `[idea]`, намеренный отказ → ADR,
порядок работ → `PLAN.md`.
9. `docs/review-brief.md`, если заводился, удалить: его разделы разошлись по
документам канона.
10. `conventions.md``conventions/`, `local-research.md``research/`.
11. Завести `docs/security.md` с периметром первой строкой и `docs/adr/`.
12. В `CLAUDE.md`: severity рядом с каждым инвариантом; семантика гейта (чем
краснеет безусловно, где логи, чего в нём нет и кто тогда гоняет дорогое);
**имя основной ветки**; запреты с путями; где `testdata` и куда писать
временное; **что считается необратимым**; общий станок; ориентир по размеру
спринта. Убрать раздел «Процесс», если он пересказывает пайплайн.
13. В `openspec/config.yaml` оставить только нужды генерации и ссылки.
14. Добавить шаг `docs.py check` в гейт проекта.
**Копии правил в шаблонах, которые версия 1 уносит в проект** — их правка в
каноне обязана появляться здесь отдельной версией:
| Что копируется | Дом определения |
| --- | --- |
| форма записи журнала дефектов в `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,52 @@
# Журнал версий формата задач до слияния плагинов
**Журнал закрыт.** У каталога задач была своя версия, пока его вёл отдельный
плагин `av-dev-tasks`. Версия теперь одна на всю раскладку —
действующий журнал [changelog.md](changelog.md), и переезд числа описан его
записью 1.
Запись ниже не переписана под нынешние имена: она описывает состояние, которое
было.
---
## Версия 1 — 2026-08-11
Первая объявленная версия формата. До неё каталог задач версии не имел вовсе:
формат менялся, а сказать, к какому его состоянию приведён конкретный проект,
было нечем — `tasks.py` о расхождении молчал, и отставший каталог выглядел
здоровым ровно до первой команды, которая об него спотыкалась.
**Что появилось.** Ключ `tasks` в `<каталог задач>/.tasks.json` — целое число,
версия формата. Сам файл стал **обязательным**: до сих пор он заводился только
ради имён, отличных от умолчания, и проект с умолчаниями жил без него. Версия —
не настройка, от которой можно отказаться, поэтому `init` и `adopt apply` теперь
пишут файл всегда, а `check` требует числа и сверяет его со своим.
**Что версия значит, а что нет.** Она отвечает на один вопрос — «по какой записи
журнала повышать каталог». Что записи применены **по существу**, из числа не
следует: двигают его руками, и соврать им так же легко, как любой другой
строкой. `check --fix` недостающее число не приписывает намеренно — это было бы
объявлением каталога приведённым к формату, шагов которого никто не делал.
**Чего в этой записи нет.** Переезды, случившиеся до появления числа, — каталог
из `docs/` в корень (канон 11) и отмена спринтов (канон 12) — задним числом сюда
не переписаны. Они уже названы журналом канона, и второй перечень тех же шагов
разошёлся бы с первым. Версия 1 — это формат на день её появления, что бы
проекту ни пришлось пройти до неё.
**Что сделать проекту.**
1. **Догнать формат по журналу канона, если каталог отстал.** Признаки известны
поимённо: каталог лежит в `docs/tasks/` (канон 11 велит `git mv docs/tasks
tasks` и починку относительных ссылок внутри записей), в нём есть `SPRINT.md`
или теги `sprint:<слаг>` (канон 12 велит снести файл, вернуть строки в беклог
через `check --fix` и расставить порядок грумингом). Ничего из этого нет —
каталог уже в сегодняшнем формате, и шаг пропускается.
2. **Завести `<каталог задач>/.tasks.json`**, если его нет. Имена частей в него
не переписываются: там только то, что отличается от умолчания.
3. **Записать версию**: `"tasks": 1` первым ключом.
4. `tasks.py check --dir <каталог задач>` — до отсутствия расхождений.
**Что при этом не трогается.** Записи в `items/`, индексы и `REJECTED.md` не
меняются ни строкой: версия 1 объявляет то, что уже есть, а не переделывает его.
+266
View File
@@ -0,0 +1,266 @@
# Журнал версий раскладки
Одна запись на версию. Проект знает свою версию из ключа `version` в
`.av-dev.toml`; операция `upgrade` скилла `av-dev:canon` идёт по записям
снизу вверх от версии проекта до текущей и делает то, что в них названо.
Правило записи: **что добавилось, что переехало, что удалено, что сделать
проекту**. Без последнего пункта запись бесполезна — по ней и работает
`upgrade`.
Версия — целое число. Обратной совместимости нет: есть «приведён» и «не
приведён». Версия **одна на всю раскладку** — и на документы канона, и на
каталог задач: ведёт их один плагин, и второе число означало бы только вопрос,
по какому журналу повышать.
**До слияния журналов было два**, и нумерация в них своя:
[changelog-before-merge.md](changelog-before-merge.md) — канон документов,
версии 114; [changelog-tasks-before-merge.md](changelog-tasks-before-merge.md) —
формат задач, версия 1. Оба **закрыты и не переписаны**: адрес, верный на день
записи, там и остался. Проект, отставший от канона 14, идёт сперва по ним, а
потом по этому журналу — порядок назван в записи 1.
---
## Версия 5 — 2026-08-23
**Метка задачи снята из процесса целиком**, и вместе с ней — подраздел «Триггеры
метки» в `docs/review.md`. Состав прогона ревью стал постоянным: он один и тот же
на всякой задаче, выбирать нечего, и признаки, по которым метка поднималась,
перестали что-либо решать. На месте подраздела — **«Когда звать глубокое ревью»**:
те же наблюдения проекта, но адресованные другому решению — звать ли
`av-dev:code-deep-review` по области кода.
**Что переехало в проекте.** Скелет `docs/review.md`, раздел настройки конвейера:
подраздел «Триггеры метки» заменён подразделом «Когда звать глубокое ревью» —
**двумя списками**: области, которые смотрят целиком (узлы с частым возвратом,
места с историей инцидентов, код под дорогое решение), и **необратимое здесь**
что в этом проекте после мерджа не откатывается обратной правкой. Второй список
работает и в цикле задачи: находка в таком месте уходит человеку развилкой, а не
чинится молча. Само правило — в [canon.md](canon.md), раздел `review.md`.
**Что сделать проекту.**
1. **Переписать подраздел в `docs/review.md`.** Заголовок «Триггеры метки»
становится «Когда звать глубокое ревью», содержимое — два списка выше.
Признаки, годные только для выбора метки («больше N файлов», «затронуто больше
одного слоя»), выбрасываются: состава прогона они не меняют. Что из прежнего
списка называло **необратимое место** — переносится во второй список дословно.
2. **Пройти по документам**`grep -rniE "small|medium|large|метк" docs/`.
Найденное в `review.md`, `conventions/` и `adr/` правится по смыслу: описание
прошлого решения остаётся как свидетельство, действующая инструкция —
переписывается или снимается.
3. **Поднять версию**`docs.py bump`, последним шагом.
4. `docs.py check` — до отсутствия дрейфа.
**Чего делать не надо.** Заводить ключ `[docs] healthcheck_last` руками: он
необязательный и появится сам первым прогоном `av-dev:doc-healthcheck`. Править
прошлые записи журналов и архивные change — тоже: метка, стоявшая в них, верна
как свидетельство о том дне.
---
## Версия 4 — 2026-08-13
Слово **провенанс** снято из словаря языка проектных текстов и заменено русским.
Оно стояло в закрытом списке своих терминов с оговоркой «„источник“ рядом
называет саму запись, а не свойство» — верной, но доказывающей лишь то, что не
годится одно русское слово. Годятся два, и по смыслу они разные: **происхождение**
у числа (чем и при каких условиях получено) и **откуда** у вопроса или находки
(кто нашёл, каким проходом, из какой записи журнала).
**Что переехало в проекте.** Скелет `docs/review.md`, подраздел «Вопросы по
темам»: форма вопроса записана как `<тема>: <вопрос> (<откуда>)` вместо
`(<провенанс>)`. Само правило — в [canon.md](canon.md), раздел `review.*`;
требование к числам `research/` не изменилось по существу, изменилось слово.
**Что сделать проекту.**
1. **Поправить форму в `docs/review.md`** — строка «Форма: `<тема>: <вопрос>
(<провенанс>)`» становится «Форма: `<тема>: <вопрос> (<откуда>)`». Уже
записанные вопросы переписывать не надо: слово стояло в шаблоне, а не в них.
2. **Пройти по документам**`grep -rn "провенанс" docs/`. Найденное в
`research/` и в `adr/` заменяется на **происхождение** (речь о числе) или на
**откуда** (речь о том, из чего вопрос или находка выросли). Ничего не
нашлось — шаг закрыт строкой, это обычный исход.
3. **Поднять версию**`docs.py bump`, последним шагом.
4. `docs.py check` — до отсутствия дрейфа.
**Чего делать не надо.** Править прошлые записи журналов и архивные change:
слово, верное на день записи, остаётся верным как свидетельство.
---
## Версия 3 — 2026-08-13
Тип записи `goal` и индекс `ROADMAP.md` упразднены; у проекта появилась
**стадия** — `build` (беклог это план стройки, порядок строк значит зависимость)
или `support` (очередь правок, порядок значит важность).
Цель была зонтиком над параллельными направлениями — она нужна там, где список
работ нельзя выстроить в один порядок. У проекта, который ведёт один человек,
такого не бывает, и роадмап при этом наполовину дублировал беклог («чего ещё не
умеет» = «что осталось в списке»), а вторую половину («что уже умеет») отвечают
`openspec/specs/` и `git log` индекса.
**Что переехало.** Индекс остался один — `BACKLOG.md`. Поле меты `Секция` стало
`Категория`; теги `goal:<слаг>`, `decomposed` и раздел `Завершение` упразднены;
команды `list --goal`, `edit --goal`, `edit --section` и ключи `[tasks] roadmap`,
`[tasks] completion_heading` — тоже. Появились ключ `[tasks] stage`, команда
`tasks.py stage` и флаги `init --stage`, `adopt scan --stage`.
**Что сделать проекту. Порядок шагов обязателен**, и первый шаг — не команда:
пока в `[tasks]` лежит упразднённый ключ, **любая** подкоманда `tasks.py`
отвечает кодом 3 и работать нечем.
1. **Вычистить конфиг руками.** Из секции `[tasks]` в `.av-dev.toml` удалить
ключи `roadmap` (или `plan`) и `completion_heading`. Каждый из них — код 3 на
любой команде, и названы они здесь оба: второй легко пропустить, потому что
его упразднение не видно по имени файла.
2. **Удалить `tasks/ROADMAP.md`.** Секция `Готово` уходит вместе с ним и **не
переносится**: «что приложение умеет» отвечают спеки, «когда это появилось» —
`git log` беклога. Проект без `openspec/specs/` теряет здесь единственный
связный перечень достигнутого — если он нужен, сохрани его сам до удаления
(документом проекта, не задачами).
3. **Прогнать `tasks.py check --fix`.** Он снимет теги `goal:<слаг>` и
`decomposed`, переименует поле `Секция``Категория` и перепишет старую
форму меты — **в том числе у самих записей типа `goal`**. Записи `goal` при
этом останутся: во что превращается цель, машина не решает и говорит
`НЕОДНОЗНАЧНО`.
4. **Разобрать цели поштучно.** У каждой два исхода, и выбирает человек: она
становится задачей (`edit <слаг> --type feature|fix|chore|research`) либо
уходит (`close <слаг> --reason …`). Строки в беклоге у неё нет — её жильём
был роадмап, — и `edit --type` заведёт её сам, в первую секцию и в конец,
сказав об этом; место назначь потом. Раздел `Завершение` в теле переехавшей
записи **удали руками**: схеме нового типа он не принадлежит, и `check`
оставит о нём замечание. Задачи, носившие тег цели, живут дальше сами по
себе — разбирать их не нужно.
5. **Объявить стадию**`tasks.py stage build` или `tasks.py stage support`.
Приложение ещё строится и список работ линеен по зависимости — `build`;
работает и правится точечно — `support`. Без ключа `check` отказывает: порядок
строк нечем прочитать.
**Объявление беклог не трогает** — ни секций, ни файлов: оно называет то, что
уже верно. Поэтому проекту с несколькими полками, объявляющему `build`,
команда откажет и назовёт выход: слить полки самому (`move <слаг> --section
<куда> --reason …`), потому что порядок строк в слитом списке знает только
человек. Флаг `--sections` при объявлении не принимается — он для **смены**
стадии, где сливать просят явно.
6. **Поправить шапку `BACKLOG.md`.** Абзац про стадию теперь размечен парой
`<!-- стадия -->``<!-- /стадия -->`, и по нему `check` сверяет шапку с
конфигом. В беклоге, заведённом до этой версии, разметки нет — `stage` об
этом скажет. Возьми готовый абзац из свежего каталога (`tasks.py init` во
временном месте) или напиши сам: он объясняет, что значит порядок строк, и
читают вместо документации именно его.
7. **Поднять версию**`docs.py bump`. Последним шагом. Он двигает **одну**
запись за раз: отставшему на две записи проекту зовётся дважды, следом за
шагами каждой.
8. `docs.py check` и `tasks.py check --dir <каталог задач>` — до отсутствия
дрейфа.
**Проект, не прошедший записи 1 и 2, начинает с этой.** Их собственные шаги
велят гонять `tasks.py check` до зелёного, а он на упразднённом ключе отвечает
кодом 3 — то есть пройти их сегодня нельзя, не сделав шаг 1 отсюда. Записи от
этого не переписываются: порядок между ними прежний, добавлено одно условие
входа.
---
## Версия 2 — 2026-08-13
Скилл `doc-canon` стал `canon`: префикс называл материал (`doc-`), а скилл
занят не материалом, а **формой** — раскладкой всех частей проекта и общим
повышением версии. Ни один файл проекта от этого не переехал; сменились **путь к
скрипту** и **имя вызова**, а оба живут в проекте: первый — строкой гейта, второй
— в `CLAUDE.md` и в записях задач.
**Что переехало в вызовах.** `av-dev:doc-canon``av-dev:canon`. Прочие имена не
тронуты.
**Что сделать проекту.**
1. **Поправить шаг гейта.** Путь к `docs.py` сменился вместе с именем каталога
скилла: `skills/doc-canon/scripts/docs.py`
`skills/canon/scripts/docs.py`. Шаг, который не нашёл скрипт, обязан
краснеть, а не пропускаться, — проверь, что он краснеет.
2. **Поправить свои вызовы скилла**`grep -rn "doc-canon" --exclude-dir=.git .`
по проекту целиком: имя встречается в `CLAUDE.md`, в `Taskfile`, в записях
задач и в документах канона. Прежнее полное имя не разрешится вовсе.
3. **Поднять версию**`docs.py bump`. Последним шагом.
4. `docs.py check` и `tasks.py check --dir <каталог задач>` — до отсутствия
дрейфа.
**Проект, не прошедший запись 1, переименовывает дважды подряд** — `skills/canon/`
`skills/doc-canon/` записью 1 и обратно этой. Порядок записей от этого не
меняется: каждая исполняется на том состоянии, которое оставила предыдущая, и
прошлая запись под новое имя не переписывается.
---
## Версия 1 — 2026-08-13
Три плагина — `av-dev-docs`, `av-dev-tasks` и `av-dev-code` — слились в один,
`av-dev`. Раскол делался под раздельную установку: проект мог взять учёт работ
без документов канона или конвейер без обоих. Практика посылку не подтвердила —
подмножество не понадобилось ни разу, — а платился раскол помеченными копиями
общих правил и веткой «плагина нет» на каждый вызов соседа.
**Что переехало в проекте.** Служебных файла было два, стал один:
| Было | Стало |
| --- | --- |
| `docs/.docs.json`, ключ `canon` | `.av-dev.toml` в корне, ключ `version` |
| `docs/.docs.json`, ключ `migrations` | `.av-dev.toml`, секция `[docs]` |
| `<каталог задач>/.tasks.json`, ключ `tasks` | тот же `version`: версия теперь одна |
| `<каталог задач>/.tasks.json`, имена частей | `.av-dev.toml`, секция `[tasks]` |
Корень выбран потому, что он есть у обоих: и у проекта без `docs/`, и у проекта
без каталога задач. Формат TOML — ради комментариев: файл лежит в репозитории
проекта, и назначение числа читают из него самого, а не из документации плагина.
**Что переехало в вызовах.** Имена скиллов сменили пространство имён и получили
префикс по прежнему плагину: `av-dev-docs:canon``av-dev:doc-canon`,
`av-dev-docs:init``av-dev:doc-init`, `av-dev-docs:docs``av-dev:doc-sync`,
`av-dev-docs:healthcheck``av-dev:doc-healthcheck`, `av-dev-tasks:tasks`
`av-dev:task-track`, `av-dev-tasks:groom``av-dev:task-groom`,
`av-dev-code:openspec``av-dev:code-openspec`, `av-dev-code:resolve`
`av-dev:code-resolve`, `av-dev-code:review``av-dev:code-review`.
**Что сделать проекту.**
1. **Отставшим сперва прежние журналы.** Версия канона в `docs/.docs.json`
меньше 14 — пройди записи до 14 по
[changelog-before-merge.md](changelog-before-merge.md), и только потом эту.
Иначе повышение объявит приведённым то, чего никто не делал.
2. **Завести `.av-dev.toml`** в корне репозитория: `version = 1`, секция
`[docs]` с `migrations`, если ключ был, секция `[tasks]` с `dir` и теми
именами частей, которые в `.tasks.json` отличались от умолчаний. Комментарии
пиши свои — файл читает человек.
3. **Удалить `docs/.docs.json` и `<каталог задач>/.tasks.json`.** Прежние имена
не читаются: два дома для одной версии расходятся молча. Пока старые файлы на
месте, `docs.py check` и `tasks.py check` называют это прежней раскладкой.
4. **Переставить плагины.** `av-dev-docs`, `av-dev-tasks` и `av-dev-code`
удалить, `av-dev` поставить — команды в README репозитория плагинов.
5. **Поправить гейт проекта.** Пути к `docs.py`, `tasks.py` и `openspec.py`
сменились вместе с именами каталогов скиллов: `skills/canon/`
`skills/doc-canon/`, `skills/tasks/``skills/task-track/`,
`skills/openspec/``skills/code-openspec/`. Шаг, который не нашёл скрипт,
обязан краснеть, а не пропускаться, — проверь, что он краснеет.
6. **Поправить свои вызовы скиллов** — в `CLAUDE.md`, в `Taskfile`, в записях
задач: короткое имя разрешится в проектную копию, а прежнее полное не
разрешится вовсе.
7. **Найти, где проект читает служебный файл сам.** Шаг гейта, скрипт, шаблон —
что угодно, что брало значение из `docs/.docs.json`, чтобы не заводить факту
второй дом. Такое чтение переезжает на `.av-dev.toml` и на `tomllib` вместо
`json`: `python3 -c 'import tomllib; print(tomllib.load(open(".av-dev.toml","rb"))["docs"]["migrations"])'`.
Ищется командой `grep -rn "\.docs\.json\|\.tasks\.json" --exclude-dir=.git .`
— по проекту целиком, а не по документам: на первом же живом переезде это
нашлось в `Taskfile.yml`, и нашёл это гейт, а не человек.
8. **Поднять версию**`docs.py bump`. Последним шагом: число объявляет
пройденными шаги журнала, и раньше времени поднятое врёт.
9. `docs.py check` и `tasks.py check --dir <каталог задач>` — до отсутствия
дрейфа.
**Чего делать не надо.** Переписывать прошлые записи журналов под новые имена.
Они описывают состояния, которые были, и адрес, верный на день записи, остаётся
верным как свидетельство.
@@ -12,16 +12,19 @@
`adr/README.md` и `review.md` уезжают в репозиторий проекта и обязаны там что-то
говорить; определение при этом остаётся в [canon.md](canon.md). Отсюда
обязанность: **правка такого правила в каноне тянет запись в
[changelog.md](changelog.md)** с указанием, какой файл проекта поднимает
[changelog.md](changelog.md)** — и запись называет, какой файл проекта поднимает
`upgrade`. Без этого копия в проекте останется на старой версии молча.
**Каждая такая копия помечена и сверяется машиной.** Дом обрамляется
`<!-- дом: <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/BACKLOG.md](../tasks/BACKLOG.md) — «что осталось», паспорт —
«зачем и для кого».
## Цель
@@ -46,7 +49,7 @@
## Что целью не является
Граница домена. По ней архитектурный проход судит, не перенесено ли понятие
Граница домена. По ней в теме `architecture` судят, не перенесено ли понятие
через границу.
## Типовые сценарии
@@ -114,7 +117,7 @@
со строкой «запись лежит сжатой и распаковывается целиком».
```
Нет БД — файла нет, и в `docs/.pm.json` нет ключа `migrations`.
Нет БД — файла нет, и в `.av-dev.toml` нет ключа `[docs] migrations`.
## `docs/security.md`
@@ -144,8 +147,8 @@
## Что вне модели
Перечислить явно. Пустой пункт означает, что враждебный проход выдумает угрозу
сам, и находка никогда не будет исправлена.
Перечислить явно. Пустой пункт означает, что в теме `security` угрозу выдумают
за тебя, и находка никогда не будет исправлена.
```
## `docs/conventions/README.md`
@@ -166,7 +169,7 @@
| Правило | Где механизировано |
| --- | --- |
Непойманное место механизации означает, что проход по конвенциям будет
Не названное здесь место механизации означает, что проход по конвенциям будет
добросовестно проверять уже проверенное.
```
@@ -197,15 +200,16 @@
```markdown
# Журнал решений
Одна запись — одно решение. **ADR это промоут поверх архивного `design.md`**,
а не второе сочинение: запись цитирует решение и ссылается на
`openspec/changes/archive/<id>/design.md`.
Одна запись — одно решение. **ADR продвигает уже написанное решение, а не
сочиняет его заново**: запись цитирует решение и ссылается на источник —
`openspec/changes/archive/<id>/design.md`, а у решения, принятого разведкой без
изменения, на её записку.
## Когда заводить
Верно одно из трёх:
<!-- копия: adr-когда-заводить из av-dev-pm/skills/canon/references/canon.md -->
<!-- копия: adr-когда-заводить из av-dev/skills/canon/references/canon.md -->
- **дорогой откат** — переделка стоит дороже переписывания одного файла;
- **намеренный отказ** от очевидного подхода;
- **пересмотр прежнего решения** — тогда у старой записи обязателен статус
@@ -216,8 +220,9 @@
## Соглашения
- Имя файла — `ADR-ГГГГ-ММ-ДД-slug.md`, слаг английский, дата — когда решение
реально принято.
- Имя файла — `ADR-ГГГГ-ММ-ДД-slug.md`, дата — когда решение реально принято.
Слаг **английский по сути, а не транслитом**: `queue-as-table`, не
`ochered-tablicej`. Форму имени и слаг проверяет `docs.py check`.
- Записи неизменяемы: передумали — новая запись, старой ставится статус.
- Активная запись статуса не имеет. Значений два: `заменено на ADR-…` и
`устарело`; ставятся полем меты записи — `- **Статус:** …` рядом с датой и
@@ -237,7 +242,8 @@
# Краткий заголовок решения
- **Дата:** ГГГГ-ММ-ДД
- **Источник:** openspec/changes/archive/<id>/design.md
- **Источник:** openspec/changes/archive/<id>/design.md — либо записка разведки,
если решение принято без изменения
Статус ставится тем же полем и только при пересмотре:
`- **Статус:** заменено на ADR-…` либо `- **Статус:** устарело`.
@@ -255,7 +261,7 @@
## Последствия
- `+` что стало лучше.
- `` чем платим: ограничения, риски, нагрузка на поддержку.
- `` чем платим: ограничения, риски, нагрузка на сопровождение.
```
## `docs/review.md`
@@ -275,43 +281,62 @@
Находки, которые здесь выглядят убедительно и всегда неверны. Каждая — с одной
строкой «почему здесь это не дефект».
### Вопросы к проходам
### Вопросы по темам
Форма: `<имя прохода>: <вопрос> (<провенанс>)`. Главный источник — журнал ниже.
Проход, увидев свой блок, задаёт эти вопросы дополнительно к обязательным.
Форма: `<тема>: <вопрос> (<откуда>)`. Главный источник — журнал ниже. Вопрос
задаёт тот проход, который закрывает эту тему на текущем прогоне, дополнительно
к обязательным.
### Триггеры профиля
**Адресуй теме, а не имени прохода.** Проходы переезжают между скиллами и
упраздняются; вопрос, адресованный проходу, перестанет задаваться в тот день,
когда тот уедет, — и заметить это будет нечем. Тема переезд переживает.
Проектная конкретизация правила выбора профиля: что здесь считается **новым
понятием или структурной единицей** (поднимает прогон до `wide` и запускает
архитектурный проход) и **где живут правила идентичности, слияния и разбора**
(до `deep`, запускает независимую реализацию) — перечнем узлов или capability,
поимённо. Уточняет умолчания конвейера, не отменяет их; рабочее умолчание —
`standard`.
Темы ядра: `requirements`, `autotests`, `conventions`, `architecture`,
`security`, `operations`. Плюс любая своя — та, под которую проект завёл в
`docs/` **свой** документ. Документы категорий `источник` и `процессный` тем не
порождают, и адресовать вопрос `passport`, `database`, `adr`, `research` или
`review` нельзя — таких тем нет. Вопрос про границу домена адресуй
`architecture`, вопрос про хранилище и числа — `operations`.
Перечень для `deep` **производен от теста конвейера**, а не заменяет его:
правило попадает в класс, когда вариантов несколько, спека между ними не
выбирает, а неверный выбор не падает, а молча меняет смысл данных. Перечисляй
места, где этот класс здесь живёт, а не переписывай определение. Таких мест нет
вовсе — так и напиши: `deep` тогда не запускается никогда, и это законное
состояние.
### Когда звать глубокое ревью
Проектная конкретизация признаков, по которым зовут `av-dev:code-deep-review`.
**Списка два, оба поимённо — узлами, слоями или capability.**
**Области, которые смотрят целиком:** узлы, куда задачи возвращаются чаще
прочих, места с историей инцидентов, код, на который обопрётся дорогое решение.
**Необратимое здесь:** что в этом проекте после мерджа не откатывается обратной
правкой — миграции, формат на диске, публичный контракт, имена, расходящиеся по
базе. Находка в таком месте уходит человеку развилкой, а не чинится молча, и
список нужен затем, чтобы «необратимое» не решалось на глаз.
Цикл задачи проверяет корректность и механику одним и тем же составом; глубину
даёт только отдельный прогон по области, и **зовёт его человек**. Списки уточняют
признаки, а не заводят расписание.
### Недоступно проверке
Оба подраздела — **по темам**: «в теме `operations` не проверяется X» читается,
а «не проверяется X» через месяц не найдёт ни один проход.
**Не проверит ни один проход** — принципиальная граница; по факту промаха не
пересматривается.
**Перестали проверять сознательно** — что, когда и почему, со ссылкой на запись
журнала. Пересматривается **первым**, как только что-то проскочило.
Тему, у которой в проекте нет дома, сюда писать не надо: её называет план
каждого прогона, и это честнее разовой записи.
## Журнал дефектов
Запись на каждый воспроизведённый дефект, сразу, а не ретроспективно: со
временем теряется не факт, а причина непоймания.
временем теряется не факт, а то, почему дефект не поймали.
Форма:
<!-- копия: журнал-дефектов-форма из av-dev-pipeline/skills/review-pipeline/references/review-journal.md -->
<!-- копия: журнал-дефектов-форма из av-dev/skills/code-review/references/review-journal.md -->
## ГГГГ-ММ-ДД — <краткое последствие> [проскочил|пойман]
- **Где:** путь:строка либо «конвейер, а не код»
@@ -325,6 +350,9 @@
<!-- /копия: журнал-дефектов-форма -->
```
Пара маркеров `копия:` внутри — машинерия маркетплейса; в `docs/review.md`
проекта уезжает только содержимое между ними (см. выше).
Новый проект: «Дефектов пока не было. Настройка конвейера появится с первым
ревью.»
@@ -372,9 +400,10 @@ severity стоит здесь, а не выводится каждым прох
- **Основная ветка:** <имя>
- **Необратимое** (спрашивается у человека всегда):
- **Общий станок** — какая проверка, покраснев, врывается в замороженный спринт:
- **Ориентир по размеру спринта:** 5–8 задач, ориентир а не закон
- **Что такое «сделана»:** пайплайн проекта пройден + критерии приёмки проверены
- **Что считается сломанным** — какая красная проверка обгоняет развитие,
то есть останавливает текущую работу:
- **Ориентир по размеру порции:** своё число, если замерялось
- **Что такое «сделана»:** конвейер проекта пройден + критерии приёмки проверены
поимённо
## Язык
@@ -388,15 +417,47 @@ severity стоит здесь, а не выводится каждым прох
тронуть рабочие данные, без третьего вся шкала ранжирования триажа держится на
догадке.
## `docs/.pm.json`
## `openspec/config.yaml`
```json
{
"canon": 2
}
**Образец переехал.** Файл заводит и заполняет скилл
`av-dev:code-openspec`, — потому что по OpenSpec работает он, а не канон
документов. Проект, не ведущий задачи циклом SDD, каталога `openspec/` не имеет
вовсе, и образец
файла, которого у него нет, в скелетах канона лежал бы мёртвым грузом.
**Форму не проверяет и `docs.py`** — с канона 10 он о файле молчит вовсе.
Проверяет её тот же владелец: скилл `av-dev:code-openspec`, команда
`openspec.py check`. Плагина конвейера в проекте может не быть — тогда форму не
смотрит никто, и это строка доклада, а не поломка. Что канон о файле всё же
говорит (единственный дом, а не форма) — [canon.md](canon.md), раздел
`openspec/config.yaml`.
## `.av-dev.toml`
```toml
# Раскладка av-dev в этом проекте: версия и настройки проверок.
version = <текущая версия>
[docs]
# migrations = "<путь>" — появится, когда появится БД
[tasks]
dir = "tasks"
```
Плюс `"migrations": "<путь>"`, если есть БД. Ключ `"tasks"` заводится **только**
когда имя файла или заголовка отличается от умолчания (`{"backlog":
"INDEX.md"}`); секций беклога в нём нет — их дом заголовки `##` индекса. Состав
ключей — [canon.md](canon.md).
`<текущая версия>` подставляет `init` или `adopt`, целым числом; берётся она из
`docs.py version` (строка «версия раскладки, скрипт»), а не из памяти. Литерал здесь
протухает при каждом повышении, поэтому его тут и нет: незамещённый плейсхолдер
ломает разбор TOML громко, а отставшее число дало бы дрейф молча.
Комментарии в файле — не украшение, а причина, по которой взят TOML: файл живёт
в чужом репозитории, и назначение числа читают из него самого. Скрипты это
учитывают и правят строку, а не переписывают файл. Состав ключей —
[canon.md](canon.md), раздел `.av-dev.toml`.
Файл лежит **в корне репозитория**, а не в `docs/`: версия одна на всю
раскладку, и нужна она в том числе проекту, который канон документов ещё не
завёл. Прежние `docs/.docs.json` и `<каталог задач>/.tasks.json` остались от
трёх плагинов, слившихся в один; увидев их, `docs.py check` называет это прежней
раскладкой и зовёт `upgrade`.
+763
View File
@@ -0,0 +1,763 @@
#!/usr/bin/env python3
"""Проверка раскладки документов проекта против канона av-dev.
Определение канона references/canon.md рядом со скриптом. Здесь только
механизируемая часть: пути, лишние файлы, битые ссылки, версия, плейсхолдеры,
маркеры долга и две сверки с кодом. Смысловые дубли и оставшееся в архитектуре
поведение судит агент скрипт об этом говорит вслух в конце отчёта.
Коды выхода общий словарь скриптов av-dev; дом словаря и разбор «дрейф
против окружения» av-dev/shared/axes.md. Значения в константах ниже.
"""
from __future__ import annotations
import argparse
import importlib.util
import re
import subprocess
import sys
from dataclasses import dataclass, field
from pathlib import Path
from types import ModuleType
from typing import NoReturn
OK, DRIFT, USAGE, ENV, INTERNAL = 0, 1, 2, 3, 4
def _load_shared() -> ModuleType:
"""Общий читатель `.av-dev.toml` — `shared/config.py` этого же плагина.
Путь считается от файла скрипта, а не от рабочего каталога: скрипт зовут из
репозитория проекта, где ни плагина, ни его дерева в текущем каталоге нет.
Своё дерево единственное, куда ходить можно; в чужое не ходим никогда.
"""
path = Path(__file__).resolve().parents[3] / "shared" / "config.py"
# Проверка именно файлом: `spec_from_file_location` на отсутствующем пути
# возвращает исправный спек, и падает уже `exec_module` — трейсбеком и кодом
# 1, то есть «найден дрейф, чинится». Битая установка дрейфом не является.
spec = importlib.util.spec_from_file_location("avdev_config", path)
if not path.is_file() or spec is None or spec.loader is None:
print(f"ОТКАЗ: не читается {path} — общий читатель настроек;"
f" переустанови плагин av-dev", file=sys.stderr)
sys.exit(ENV)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
conf = _load_shared()
# Версия раскладки одна на плагин и живёт в `shared/config.py`: её знают оба
# скрипта, и второе число здесь было бы вторым домом.
LAYOUT_VERSION = conf.VERSION
# Дом версии и путей, нужных проверкам, — `.av-dev.toml` в корне репозитория.
# До слияния плагинов файлов было два, `docs/.docs.json` и `.tasks.json`, и
# версии двигались порознь; теперь дом один, и лежит он в корне, потому что
# настройки нужны и проекту без `docs/`.
CONFIG = conf.CONFIG_NAME
# --- Раскладка канона -------------------------------------------------------
# Документ канона: имя → (категория, на какой вопрос отвечает).
#
# Категории — из canon.md, раздел «Три категории документов». Разрез один: можно
# ли по документу сказать «в этом изменении сделано не так»?
# тема — да, прямо: документ заводит направление проверки изменения;
# источник — нет, но он задаёт границу, по которой судит чужая тема;
# процессный — нет: он про то, как мы работаем, а не про изменение.
#
# **Категория не меняет обязательности документа** — заводятся все три
# одинаково и с первого дня. Она меняет только то, что с документом делает
# конвейер ревью, и потому печатается в отказе: «нет источника passport»
# читается иначе, чем «нет темы security», и чинится теми же руками, но с
# другим приоритетом.
#
# **Документ живёт файлом `docs/<имя>.md` либо каталогом `docs/<имя>/` с
# README.md внутри.** Форму выбирает проект: документ разросся — стал каталогом,
# и это не смена канона и не повод править скрипт. Обе формы сразу — ошибка: это
# два дома для одного факта, ровно то, от чего канон и защищает.
DOCS = {
"passport": ("источник", "зачем и для кого, чем НЕ является"),
"architecture": ("тема", "как сложено — обзор, окружение, эксплуатация"),
"security": ("тема", "периметр, недоверенный вход, что вне модели"),
"conventions": ("тема", "как мы пишем код; индекс, промоут, что механизировано"),
"research": ("процессный", "что показала реальность: наблюдения и числа"),
"adr": ("процессный", "почему решено так; индекс, статусы, правило замены"),
"review": ("процессный", "настройка конвейера + журнал дефектов"),
}
# Документ, обязательный только при условии: имя → (ключ .docs.json, категория,
# пояснение).
CONDITIONAL_DOCS = {
"database": ("migrations", "источник", "схема хранилища и настройки"),
}
# Обязательные файлы вне раскладки docs/.
REQUIRED = {
"CLAUDE.md": "памятка агенту: инварианты с severity, команды, семантика гейта",
CONFIG: "версия раскладки av-dev и пути, нужные проверкам",
}
# Файлы, которые документ-каталог обязан держать сверх README.md.
DOC_EXTRA = {
"adr": {"template.md": "шаблон записи ADR"},
}
# Служебное в docs/ и каталог задач, оставшийся там от прежней раскладки. Формы
# у них скрипт не проверяет, и по разным причинам: `.docs.json` не markdown (и
# сам он теперь след прежней раскладки, о котором говорит `check_required`), а
# задачи ведёт **другой скилл** — `task-track`, со своим скриптом и своими
# проверками.
#
# Дом задач с версии 11 — `tasks/` в корне репозитория, то есть вне `docs/`
# вовсе. `docs/tasks/` здесь терпится потому, что непереехавший проект не должен
# получать «файл вне канона» вдобавок к записи журнала, которая и так велит ему
# переехать. Внутрь скрипт не смотрит ни в том, ни в другом случае.
#
# Прежнее имя конфига терпится ровно за тем же: про переименование проект
# слышит одну строку — от `check_required`, — а не две, из которых вторая ещё и
# зовёт файл лишним.
NOT_DOCS = {".docs.json", ".pm.json", "tasks"}
# Слоты, которых в каноне нет, — с адресом, куда уезжает содержимое. Имена,
# совпадающие с темой, отсюда убраны намеренно: `docs/conventions.md` и
# `docs/review/` теперь законные формы своих тем.
RETIRED = {
"review-brief.md": "документы канона и есть бриф; остаток — в review",
"review-journal.md": "→ документ review",
"plan.md": "→ tasks/BACKLOG.md (ведёт скилл task-track)",
"local-research.md": "→ документ research",
"specs": "поведение → openspec/specs/, обзор → тема architecture",
"drafts": "идея → запись research, отказ → ADR, порядок → BACKLOG.md",
"backlog": "→ tasks/ в корне репозитория (ведёт скилл task-track)",
}
# --- Слаги в именах файлов --------------------------------------------------
# Текст документов русский, а **имена файлов английские, kebab-case**. Причина
# не в эстетике: имя файла стоит в ссылках из других документов, в коммитах и в
# путях, которые люди набирают руками, — а кириллица в пути ломается по-разному
# в разных местах и не набирается на английской раскладке.
SLUG = re.compile(r"[a-z0-9]+(?:-[a-z0-9]+)*")
ADR_NAME = re.compile(r"ADR-(\d{4})-(\d{2})-(\d{2})-(.+)")
CYRILLIC = re.compile(r"[а-яёА-ЯЁ]")
# Признаки транслита — и только они. Отличить английское слово от транслита
# машина не умеет, поэтому находка идёт **замечанием**: кластеры, которых в
# английском практически не бывает, плюс окончания русских падежей.
#
# Слабые маркеры выброшены намеренно, каждый по своему ложному срабатыванию:
# `ost` ловит `post` и `cost`, `sch` — `schema`, `ya` — `yaml`, `nost` —
# `nostalgia`, хвост `ii` — `radii`. Набор подобран так, чтобы ложных
# срабатываний не было вовсе: правило, краснеющее на правде, приучает
# пролистывать весь блок. Цена известна и принята — `sostoyanie-partii`
# проходит мимо.
#
# Тот же приём, что `translit_ish` в tasks.py; скрипты независимы намеренно —
# каждый уезжает в чужой проект в одиночку.
TRANSLIT_CLUSTER = re.compile(r"zh|kh|shch|tsy|iya|ovanie|enie|stvo")
TRANSLIT_TAIL = re.compile(r"(?:ej|oj|ij|yj|yy|aya)$")
def translit_ish(slug: str) -> bool:
if TRANSLIT_CLUSTER.search(slug):
return True
return any(TRANSLIT_TAIL.search(part) for part in slug.split("-"))
def check_slugs(root: Path, rep: Report) -> None:
"""Имена файлов канона: латиница kebab-case, у ADR — ещё и форма имени.
Каталог задач не трогаем: его слаги ведёт и проверяет tasks.py, и вторая
проверка того же места разошлась бы с первой.
"""
docs = root / "docs"
if not docs.is_dir():
return
# Имена, выбранные каноном, а не проектом: их форма задана здесь же.
fixed = {"README.md", "template.md"} | {f"{name}.md" for name in DOCS}
# Все документы-каталоги, включая свои темы проекта: правило имён общее, а
# перечислять их поимённо значило бы закрыть открытый список.
for folder in sorted(docs.iterdir()):
if not folder.is_dir() or folder.name in NOT_DOCS:
continue
sub = folder.name
for path in sorted(folder.rglob("*.md")):
name = path.name
rel = path.relative_to(root)
if name in fixed:
continue
stem = path.stem
if sub == "adr":
m = ADR_NAME.fullmatch(stem)
if not m:
rep.error(
f"{rel}: имя не по форме ADR-ГГГГ-ММ-ДД-slug.md — "
f"по имени сортируются записи и ищется дата решения"
)
continue
stem = m.group(4)
if CYRILLIC.search(stem):
rep.error(
f"{rel}: кириллица в имени файла — слаги английские, "
f"kebab-case (текст документа при этом русский)"
)
continue
if not SLUG.fullmatch(stem):
rep.error(
f"{rel}: имя не kebab-case латиницей — только строчные "
f"буквы, цифры и одиночные дефисы"
)
continue
if translit_ish(stem):
rep.note(
f"{rel}: имя похоже на транслит («{stem}») — слаг именуется "
f"английским словом по сути, а не записью русского латиницей: "
f"транслит нечитаем тому, кто ищет по смыслу. Проверено "
f"эвристикой: английское слово от транслита машина не отличает"
)
check_capability_slugs(root, rep)
def check_capability_slugs(root: Path, rep: Report) -> None:
specs = root / "openspec" / "specs"
if not specs.is_dir():
return
for folder in sorted(specs.iterdir()):
if not folder.is_dir():
continue
if CYRILLIC.search(folder.name) or not SLUG.fullmatch(folder.name):
rep.error(
f"openspec/specs/{folder.name}/: имя capability — латиница "
f"kebab-case; оно стоит в ссылках из architecture.md и в спеках"
)
DEBT_MARKER = re.compile(r"<!--\s*канон:\s*(.+?)\s*-->")
PLACEHOLDER = re.compile(r"<!--\s*заполнить:\s*(.+?)\s*-->")
MD_LINK = re.compile(r"\[[^\]]*\]\(\s*<?([^)>\s]+)>?(?:\s+[\"'(][^)]*)?\)")
FENCE = re.compile(r"^\s*(```|~~~)")
INLINE_CODE = re.compile(r"`[^`\n]*`")
def strip_code(text: str) -> str:
"""Выкинуть блоки кода и вставки в обратных кавычках.
Путь в примере или в шаблоне не ссылка, и краснеть на нём значит краснеть
на каждом образце документа. Инлайн-код тоже: `[docs/backlog](tasks/)`
в тексте про подписи ссылок иллюстрация, а не ссылка."""
out, inside = [], False
for line in text.splitlines():
if FENCE.match(line):
inside = not inside
continue
out.append("" if inside else INLINE_CODE.sub("", line))
return "\n".join(out)
@dataclass
class Report:
errors: list[str] = field(default_factory=list)
notes: list[str] = field(default_factory=list)
debts: list[str] = field(default_factory=list)
skipped: list[str] = field(default_factory=list)
def error(self, msg: str) -> None:
self.errors.append(msg)
def note(self, msg: str) -> None:
self.notes.append(msg)
def debt(self, msg: str) -> None:
self.debts.append(msg)
def skip(self, msg: str) -> None:
self.skipped.append(msg)
def fail(code: int, msg: str) -> NoReturn:
print(f"ОТКАЗ: {msg}", file=sys.stderr)
sys.exit(code)
def read_config(root: Path, rep: Report) -> dict:
"""Настройки проекта целиком; проверкам канона нужна секция `[docs]`."""
try:
cfg = conf.read(root)
conf.check_keys(docs_cfg(cfg), DOCS_KEYS, "в секции [docs]")
except conf.ConfigError as exc:
fail(ENV, str(exc))
return cfg
# Ключи секции `[docs]`. Секцию знает этот скрипт, а не общий читатель: ключ
# заводится вместе с проверкой, которая его читает.
#
# `healthcheck_last` — коммит прошлой сверки документов; пишет его скилл
# `av-dev:doc-healthcheck`, читает `av-dev:doc-sync`, чтобы сосчитать задачи с
# тех пор. Здесь он стоит **только чтобы файл не отвергли**: неизвестный ключ —
# отказ кодом 3, то есть ключ, заведённый скиллом мимо этой константы, сделал бы
# нерабочими и `docs.py`, и `tasks.py`, и гейт проекта, который их зовёт.
# Проверки, читающей его, у скрипта нет и не предполагается: значение — след
# работы человека, а не настройка.
DOCS_KEYS = ("migrations", "healthcheck_last")
def docs_cfg(cfg: dict) -> dict:
return conf.section(cfg, "docs")
# --- Проверки ---------------------------------------------------------------
def check_version(root: Path, cfg: dict, rep: Report) -> None:
if not (root / CONFIG).exists():
return # об отсутствии файла скажет check_required, второй раз не нужно
got = conf.version(cfg)
if got is None:
rep.error(f"в {CONFIG} нет ключа version — версия раскладки не объявлена")
return
if got < LAYOUT_VERSION:
rep.error(
f"проект приведён к раскладке версии {got}, текущая — {LAYOUT_VERSION}:"
f" нужно повышение (скилл av-dev:canon, операция upgrade)"
)
elif got > LAYOUT_VERSION:
rep.error(
f"проект приведён к раскладке версии {got}, а скрипт знает"
f" {LAYOUT_VERSION}: устарел плагин, обнови маркетплейс"
)
def doc_home(root: Path, name: str) -> tuple[Path | None, str | None]:
"""Дом документа: файл `docs/<имя>.md` или каталог `docs/<имя>/`.
Возвращает путь и жалобу. Обе формы сразу это два дома для одного факта, и
расходятся они молча: правят одну, читают другую.
"""
docs = root / "docs"
as_file = docs / f"{name}.md"
as_dir = docs / name
if as_file.is_file() and as_dir.is_dir():
return as_file, (
f"{name} живёт сразу двумя домами — docs/{name}.md и docs/{name}/:"
f" оставить один, иначе правят один, а читают другой"
)
if as_file.is_file():
return as_file, None
if as_dir.is_dir():
if not (as_dir / "README.md").is_file():
return as_dir, (
f"docs/{name}/ без README.md — у документа-каталога вход"
f" обязателен: по нему его читают агенты"
)
return as_dir, None
return None, None
def check_legacy(root: Path, rep: Report) -> None:
"""Следы прежней раскладки — отдельная проверка, а не ветка отсутствия.
Пока она жила внутри «нового файла нет», половина переезда проходила молча:
завели `.av-dev.toml`, старые файлы удалить забыли и оба скрипта считали
проект здоровым. Это ровно тот второй дом, против которого переезд и
делался, и увидеть его можно только тогда, когда новый файл уже есть.
"""
legacy = conf.legacy_files(root)
if not legacy:
return
if (root / CONFIG).is_file():
rep.error(
f"прежняя раскладка не убрана: {', '.join(legacy)} рядом с {CONFIG}."
f" Эти файлы не читаются, и версия в них своя — второй дом для того"
f" же числа. Удали их: переезд не закончен (журнал, версия 1, шаг 3)"
)
return
rep.error(
f"нет {CONFIG}, а настройки лежат по прежней раскладке"
f" ({', '.join(legacy)}) — она осталась от трёх плагинов, которые"
f" слились в один: перенеси значения и удали старые файлы операцией"
f" upgrade скилла av-dev:canon (журнал, версия 1). Прежние имена не"
f" читаются, поэтому в этом прогоне всё остальное проверено так, будто"
f" настроек нет вовсе"
)
def check_required(root: Path, cfg: dict, rep: Report) -> None:
for rel, what in REQUIRED.items():
if (root / rel).exists():
continue
if rel == CONFIG and conf.legacy_files(root):
continue # об этом уже сказала check_legacy, и подробнее
rep.error(f"нет {rel}{what}")
for name, (kind, what) in DOCS.items():
home, complaint = doc_home(root, name)
if home is None:
rep.error(
f"нет документа {name} (docs/{name}.md или docs/{name}/),"
f" категория «{kind}» — {what}"
)
continue
if complaint:
rep.error(complaint)
if home.is_dir():
for extra, why in DOC_EXTRA.get(name, {}).items():
if not (home / extra).is_file():
rep.error(f"нет docs/{name}/{extra}{why}")
docs = docs_cfg(cfg)
for name, (key, kind, what) in CONDITIONAL_DOCS.items():
home, complaint = doc_home(root, name)
if complaint:
rep.error(complaint)
if key in docs and home is None:
rep.error(
f"нет документа {name} (docs/{name}.md или docs/{name}/),"
f" категория «{kind}» — {what}"
f" (обязателен: в {CONFIG} объявлен [docs] {key})"
)
elif key not in docs and home is None:
rep.skip(f"{name} — в {CONFIG} нет ключа [docs] {key},"
f" проверка неприменима")
def check_stray(root: Path, rep: Report) -> None:
"""Лишнего в docs/ больше нет — есть свои темы проекта.
Категории `источник` и `процессный` **закрыты**: они перечислены в каноне
поимённо и проектом не пополняются. Открыта только категория `тема`
поэтому любой документ в docs/, которого нет в раскладке, и есть заявка на
свою тему, и запретить её нельзя. Проверяются только слоты, у которых дом в
другом месте, иначе переехавшее содержимое вернулось бы темой и выглядело
законным.
"""
docs = root / "docs"
if not docs.is_dir():
rep.error("нет каталога docs/")
return
known = set(DOCS) | set(CONDITIONAL_DOCS)
own: list[str] = []
for entry in sorted(docs.iterdir()):
name = entry.name
if name in RETIRED:
rep.error(f"docs/{name} — слота нет в каноне: {RETIRED[name]}")
continue
if name in NOT_DOCS:
continue
topic = name[:-3] if entry.is_file() and name.endswith(".md") else name
if topic in known:
continue
if entry.is_file() and not name.endswith(".md"):
rep.error(f"docs/{name} — не markdown: тема ревью читается как текст")
continue
if entry.is_dir() and not (entry / "README.md").is_file():
rep.error(
f"docs/{name}/ без README.md — у темы-каталога вход обязателен:"
f" по нему её читают агенты"
)
continue
own.append(topic)
if own:
rep.note(
f"свои темы проекта: {', '.join(own)} — именной оптики у них нет,"
f" их разбирает общий проход конвейера"
)
def canon_docs(root: Path) -> list[Path]:
"""Документы канона. Каталог задач ведёт tasks.py; упразднённые каталоги
уже названы отдельной строкой, и их внутренние ссылки не наша забота
они переезжают целиком."""
out = []
docs = root / "docs"
skip = {"tasks"} | {name for name in RETIRED if not name.endswith(".md")}
if docs.is_dir():
for path in sorted(docs.rglob("*.md")):
head = path.relative_to(docs).parts[0]
if head in skip or head in RETIRED:
continue
out.append(path)
# AGENTS.md лежит рядом с CLAUDE.md и читается теми же агентами: он почти
# стандарт, и проект вправе держать оба. Обязателен по-прежнему только
# первый.
for name in ("CLAUDE.md", "AGENTS.md"):
path = root / name
if path.exists():
out.append(path)
return out
def check_links(root: Path, rep: Report) -> None:
for path in canon_docs(root):
try:
text = path.read_text(encoding="utf-8")
except OSError as exc:
rep.error(f"{path.relative_to(root)} не читается: {exc}")
continue
for target in MD_LINK.findall(strip_code(text)):
target = target.strip()
if not target or target.startswith(("http://", "https://", "#", "mailto:")):
continue
clean = target.split("#", 1)[0]
if not clean:
continue
if (path.parent / clean).exists():
continue
rep.error(f"{path.relative_to(root)}: битая ссылка на {target}")
def check_placeholders_and_debt(root: Path, rep: Report) -> None:
for path in canon_docs(root):
text = strip_code(path.read_text(encoding="utf-8", errors="replace"))
rel = path.relative_to(root)
for what in PLACEHOLDER.findall(text):
# Замечание, а не дрейф: незаполненный канон — объявленное переходное
# состояние, и краснеть на нём значит требовать выдумать содержание.
rep.note(f"{rel}: плейсхолдер шаблона не заполнен — {what}")
for what in DEBT_MARKER.findall(text):
rep.debt(f"{rel}: {what}")
def doc_text(root: Path, name: str) -> str | None:
"""Текст документа целиком: файл или все markdown каталога, склеенные.
Проверке всё равно, одним файлом написан документ или десятью: она ищет
упоминание, а упоминание живёт в любом из них.
"""
home, _ = doc_home(root, name)
if home is None:
return None
if home.is_file():
return home.read_text(encoding="utf-8", errors="replace")
return "\n".join(
path.read_text(encoding="utf-8", errors="replace")
for path in sorted(home.rglob("*.md"))
)
def check_capabilities(root: Path, rep: Report) -> None:
specs = root / "openspec" / "specs"
text = doc_text(root, "architecture")
if not specs.is_dir():
rep.skip("openspec/specs/ нет — сверка capability с архитектурой неприменима")
return
if text is None:
rep.skip(
"темы architecture нет — capability не сверены с обзором "
"(об отсутствии сказано отдельной строкой)"
)
return
for d in sorted(specs.iterdir()):
if not d.is_dir():
continue
name = d.name
# Засчитываем только явное упоминание: ссылку на спеку или имя в обратных
# кавычках. Голая подстрока совпадает с именем пакета или CLI-команды и
# даёт ложное «упомянуто» — то есть проверку, проходящую не по той причине.
explicit = f"openspec/specs/{name}" in text or f"`{name}`" in text
loose = re.search(rf"\b{re.escape(name)}\b", text) is not None
if explicit:
continue
if loose:
rep.note(
f"capability {name}: в теме architecture есть слово «{name}», но "
f"нет ни ссылки на openspec/specs/{name}, ни имени в обратных "
f"кавычках — проверь, это про capability или про пакет"
)
else:
rep.error(
f"capability {name} есть в openspec/specs/, но не упомянута в "
f"теме architecture — обзор отстал от нормативных спек"
)
def changed_files(root: Path, base: str, rep: Report) -> list[str] | None:
"""Объединение закоммиченного, рабочего дерева и untracked.
Гейт гоняют ДО коммита, поэтому `base...HEAD` не видит ровно ту правку, ради
которой проверка и заводилась: миграция уже лежит в дереве, но ещё не в
истории. Пропущенная правка выглядела бы как зелёный шаг."""
cmds = [
["diff", "--name-only", base],
["ls-files", "--others", "--exclude-standard"],
]
seen: list[str] = []
for cmd in cmds:
try:
out = subprocess.run(
["git", "-C", str(root), *cmd],
capture_output=True,
text=True,
check=True,
)
except (subprocess.CalledProcessError, FileNotFoundError) as exc:
rep.skip(f"сверка миграций пропущена: git не отдал дифф ({exc})")
return None
seen.extend(line for line in out.stdout.splitlines() if line)
return sorted(set(seen))
def check_migrations(root: Path, cfg: dict, base: str | None, rep: Report) -> None:
migrations = docs_cfg(cfg).get("migrations")
if not migrations:
rep.skip(f"в {CONFIG} нет ключа [docs] migrations —"
f" сверка со схемой неприменима")
return
if not base:
rep.skip("база диффа не названа (--base) — сверка миграций со схемой не гонялась")
return
changed = changed_files(root, base, rep)
if changed is None:
return
touched = [f for f in changed if f.startswith(migrations.rstrip("/") + "/")]
if not touched:
return
# Тема database бывает файлом и каталогом — правкой считается любой её файл.
if not any(
f == "docs/database.md" or f.startswith("docs/database/") for f in changed
):
rep.error(
f"миграции изменены ({len(touched)} файлов), а тема database — нет: "
f"схема в документации отстала"
)
# --- Отчёт ------------------------------------------------------------------
def report(rep: Report) -> int:
for msg in rep.errors:
print(f"ДРЕЙФ {msg}")
for msg in rep.notes:
print(f"ЗАМЕЧАНИЕ {msg}")
if rep.debts:
print(f"\nДОЛГ ({len(rep.debts)} маркеров, гейт от них не краснеет):")
for msg in rep.debts:
print(f" {msg}")
if rep.skipped:
print("\nНЕ ПРОВЕРЯЛОСЬ:")
for msg in rep.skipped:
print(f" {msg}")
print(
"\nМашина проверила раскладку, имена файлов, ссылки, версию и две\n"
"сверки с кодом. Форму openspec/config.yaml она не проверяет: каталог\n"
"принадлежит конвейеру, и форму смотрит его скрипт\n"
"(`av-dev:code-openspec`, команда `openspec.py check`). Согласованность\n"
"документов между собой и с кодом — тоже не её: это суждение агентов\n"
"`doc-consistency` (документ ↔ документ ↔ openspec) и `doc-code-drift`\n"
"(документ ↔ код)."
)
if rep.errors:
print(f"\nИтог: дрейф, {len(rep.errors)} пунктов.")
return DRIFT
print("\nИтог: канон соблюдён в механизируемой части.")
return OK
def cmd_check(args: argparse.Namespace) -> int:
root = Path(args.dir).resolve()
if not root.is_dir():
fail(ENV, f"каталог {root} не найден")
if not (root / "docs").exists() and not (root / "CLAUDE.md").exists():
fail(ENV, f"{root} не похож на корень проекта: нет ни docs/, ни CLAUDE.md")
rep = Report()
cfg = read_config(root, rep)
check_version(root, cfg, rep)
check_legacy(root, rep)
check_required(root, cfg, rep)
check_stray(root, rep)
check_slugs(root, rep)
check_links(root, rep)
check_placeholders_and_debt(root, rep)
check_capabilities(root, rep)
check_migrations(root, cfg, args.base, rep)
return report(rep)
def cmd_version(args: argparse.Namespace) -> int:
root = Path(args.dir).resolve()
if not root.is_dir():
fail(ENV, f"нет каталога {root}")
cfg = read_config(root, Report())
got = conf.version(cfg)
print(f"версия раскладки, скрипт: {LAYOUT_VERSION}")
print(f"версия раскладки, проект: {got if got is not None else 'не объявлена'}")
return OK
def cmd_bump(args: argparse.Namespace) -> int:
"""Поднять версию проекта до той, что знает скрипт. Последний шаг повышения.
Двигается **строка**, а не файл: комментарии в нём принадлежат проекту.
Поднять раньше времени нельзя не потому, что скрипт не даст, а потому что
число объявляет пройденными шаги журнала, которых никто не делал, поэтому
команда отдельная и зовётся руками, а `check --fix` этого не пишет.
"""
root = Path(args.dir).resolve()
if not (root / CONFIG).is_file():
fail(ENV, f"нет {root / CONFIG} — сперва заведи раскладку (adopt)")
was = conf.version(read_config(root, Report()))
if was == LAYOUT_VERSION:
print(f"версия уже {LAYOUT_VERSION}, файл не тронут")
return OK
if was is not None and was > LAYOUT_VERSION:
fail(ENV, f"проект на версии {was}, скрипт знает {LAYOUT_VERSION}:"
f" устарел плагин, обнови маркетплейс")
# Двигается **одна** запись за раз, а не сразу до текущей: число объявляет
# пройденными шаги журнала, и прыжок через запись объявил бы пройденным то,
# чего никто не делал. Отставшему на три записи проекту `bump` зовётся три
# раза — по разу на запись, следом за её шагами.
#
# Версии нет вовсе — случай другой: проект не жил ни одной записью журнала,
# его раскладку только что вывели сегодняшним форматом (`adopt`), и
# объявлять ему нечего, кроме текущего числа.
target = LAYOUT_VERSION if was is None else was + 1
conf.set_version(root, target)
print(f"версия раскладки: {was if was is not None else 'не была объявлена'}"
f"{target} в {CONFIG}")
if target < LAYOUT_VERSION:
print(f" до текущей ({LAYOUT_VERSION}) осталось записей журнала:"
f" {LAYOUT_VERSION - target}. Пройди шаги следующей и позови bump"
f" снова — по разу на запись")
return OK
def main() -> int:
parser = argparse.ArgumentParser(
prog="docs.py",
description="механическая проверка канона документов проекта",
)
sub = parser.add_subparsers(dest="cmd", required=True)
p_check = sub.add_parser("check", help="раскладка, ссылки, версия, сверки с кодом")
p_check.add_argument("--dir", default=".", help="корень проекта (по умолчанию текущий)")
p_check.add_argument("--base", default=None, help="база диффа для сверки миграций")
p_check.set_defaults(func=cmd_check)
p_ver = sub.add_parser("version", help="версия раскладки: скрипта и проекта")
p_ver.add_argument("--dir", default=".", help="корень проекта")
p_ver.set_defaults(func=cmd_version)
p_bump = sub.add_parser("bump", help="поднять версию проекта до версии скрипта")
p_bump.add_argument("--dir", default=".", help="корень проекта")
p_bump.set_defaults(func=cmd_bump)
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())
+243
View File
@@ -0,0 +1,243 @@
---
name: code-deep-review
description: "Глубокое ревью области кода — не задачи, а куска проекта: модуля, слоя, сервиса целиком. Зовёт проходы, которых нет в цикле задачи: review-adversary (строит путь и прогоняет падающий тест), review-ops (снимает числа замером), review-architecture на входе шире диффа и по форме решения, review-code по коду целиком, а сводит их review-triage. Здесь единственное место процесса, где форму решения судят после кода и где находка доказывается прогоном и замером. Проходы, помеченные «держит машину», идут цепочкой. Исход — не правки, а разговор: находки предлагаются человеку, обсуждаются по одной, и согласованное уезжает задачами через av-dev:task-track, сценарий «задачи из ревью и аудита». Использовать время от времени и по признаку: накопился десяток задач в одной области, перед тем как опереться на узел в дорогом решении, после инцидента, по строке «отложено в code-deep-review» из отчётов ревью. Дорого — не на задаче и не по расписанию. Ревью одного изменения — скилл av-dev:code-review."
---
# Глубокое ревью области
Смотрит **не задачу, а место в проекте**: модуль, слой, сервис целиком. Отсюда и
всё остальное устройство — вход, состав проходов, исход.
Разрез с конвейером задачи проверяемый: **`av-dev:code-review` судит изменение,
этот скилл судит написанное**. Там вход — дифф и дельта-спеки, здесь — область
кода и её история. Там исход — правки в том же прогоне, здесь — разговор и
задачи.
## Зачем он появился
Тяжёлые проходы стояли в цикле задачи: `review-adversary` строил путь и прогонял
падающий тест, `review-ops` снимал числа замером, `review-architecture` судил
форму решения на входе шире диффа. Первые двое держали машину и шли цепочкой,
третий требовал карты проекта; все трое стоили часов **на каждой задаче**, где
запускались, — при том что их ценность оплачивается на каждой, а получается на
немногих.
Их вынесли сюда целиком, и цикл задачи после этого проверяет **корректность и
механику**: заказанное против сделанного, дефект, который сработает сам,
конвенции проекта и сверку с записанными инвариантами `CLAUDE.md`. Темы
`security`, `operations` и `architecture` остались там ровно в объёме
инвариантов — свойства, которого в них нет, цикл не спросит.
**Вход этому скиллу копят проходы цикла.** Строка «отложено в
`av-dev:code-deep-review`» в границах покрытия называет тему, место и запуск,
которым это проверяется; триаж сводит такие строки в отдельную секцию отчёта.
Второй источник — сигнал «это изменение просит глубокого ревью»: его подаёт
`review-code` всегда и `review-basics`, когда запускается.
## Когда звать
**Зовёт человек**, и признак наблюдаемый, а не календарный:
- **накопился десяток задач в одной области** — по отдельности каждая прошла
обычный цикл, а вместе они переписали узел;
- **строки «отложено» скопились**: в отчётах ревью по одному месту повторяется
один и тот же неснятый замер;
- **перед дорогим решением**, которое обопрётся на этот узел;
- **после инцидента** — когда уже известно, где болит, и надо понять, что рядом;
- **узел, в который возвращаются третий раз**: цикл задачи проверяет его каждый
раз заново и одним и тем же составом, а здесь это повод посмотреть узел целиком.
**Не на задаче и не по расписанию.** Цена реальная: два прохода держат машину и
идут цепочкой, вход шире диффа собирается командой проекта, а разбор находок
требует человека. Прогон по каждой задаче был бы ровно той церемонией, ради
снятия которой проходы отсюда и переехали.
## Чего может не быть
**Копия.** Дом правила — `shared/absence.md` в репозитории плагина.
Правится дом, а не этот файл.
<!-- копия: отсутствие из av-dev/shared/absence.md -->
**Скилл не вправе считать раскладку проекта полной.** Части заводятся порознь и
живут порознь; каждая узнаётся своим следом:
| Чего нет | Как видно | Чего теперь не делает никто |
| --- | --- | --- |
| настройки av-dev | нет `.av-dev.toml` в корне | проект под процесс не заводился; версии нет, настроек нет |
| документы канона | нет `docs/` | проектную конкретику брать неоткуда — темы, инварианты, прецеденты |
| учёт работ | нет каталога задач | запись остаётся владельцу: назови её текстом в докладе |
| источник требований | нет `openspec/config.yaml` | цикл SDD не запускается: спеки не с чем сверять |
**Свой скилл зовётся полным именем** — `av-dev:canon`, `av-dev:task-track`,
`av-dev:code-review`. Короткое имя может разрешиться в устаревшую проектную
копию из `.claude/skills/`, и подмены не будет видно ни в докладе, ни в
поведении.
**Внешний плагин может не стоять.** Их два: `opsx:*` — цикл SDD, и
`av-dev-git:commit` — сообщения коммитов. Путь в дерево чужого плагина не
пишется никогда: `$CLAUDE_PLUGIN_ROOT` ведёт только в своё дерево, а
вычисленный от него путь к соседу либо не откроется, либо откроет чужую
установку. Нужен чужой справочник — зови владеющий им скилл, он прочитает его
сам.
**Отсутствие — исход, а не поломка.** Назови строкой доклада, чего теперь не
делает никто, и продолжай работу. Молчать нельзя: пропуск неотличим от
сделанного. Выдумывать обходной путь нельзя тоже.
**Присутствие узнаётся следом в проекте, а не объявлением.** Перечня того, что
здесь заведено, проект не ведёт — он разошёлся бы с действительностью молча.
<!-- /копия: отсутствие -->
**Каталога задач нет** — находки остаются списком в докладе, и это говорится
строкой: заводить их некуда, а держать в голове до следующего прогона нечем.
## Вход — область, а не дифф
**Область называет человек, и называет до запуска.** Пакет, слой, сервис,
capability — одним адресом или несколькими. Скилл область не выбирает сам: выбор
области и есть решение о том, во что вложить часы, и оно человеческое.
Область не названа — **спроси, а не бери репозиторий целиком**. Прогон по всему
проекту даёт находки, рассыпанные по местам, между которыми нет связи, а разбор
такого урожая не доводится до конца никогда.
К области собирается **корпус**:
| Что | Откуда | Зачем |
|---|---|---|
| код области целиком | адреса, названные человеком | вход всех проходов |
| история области | `git log` по этим путям | что переписывалось и сколько раз |
| отложенное | строки «отложено в `av-dev:code-deep-review`» из отчётов ревью | неснятые замеры и недостроенные пути |
| журнал дефектов | `docs/review.md` | что уже проскакивало мимо конвейера |
| дома тем | `docs/security.*`, `docs/architecture.*`, `docs/conventions.*` | против чего судить |
Отложенного нет вовсе — скажи это строкой. Пустой список значит либо что цикл
ничего не откладывал, либо что проходы не писали свою строку; вторая причина —
находка о процессе, и она идёт в доклад.
## Состав прогона
Состав **постоянный**, но глубина у проходов **разная, и это не небрежность**.
Доказательство дают те двое, что держат машину: `review-adversary` прогоняет
падающий тест, `review-ops` снимает числа замером. `review-architecture` и
`review-code` машину не держат — они дают **разбор на входе шире диффа**, и
выдать доказательство им нечем. Постоянен и состав цикла задачи, но он другой и
мельче: разница между скиллами не в старательности, а в том, что здесь запускают,
меряют и строят путь.
| Проход | Тема | Что делает |
|---|---|---|
| `review-adversary` | `security` | строит путь и **прогоняет** падающий тест |
| `review-ops` | `operations` | снимает числа замером: удержание, рост, деградация |
| `review-architecture` | `architecture` | концептуальная целостность на входе шире диффа |
| `review-code` | `conventions`, техника, инварианты | читает код **как код**, целиком, а не диффом; потолков здесь нет |
| `review-triage` | — | единственный сток: дедуп, оракулы, потолок |
**Гейта здесь нет, и это не пропуск.** Гейт судит изменение — красный он или
зелёный, к написанному месяц назад коду это не относится. Если гейт проекта
красный, скажи это строкой: находки о коде, который не собирается, стоят меньше.
**Цепочка за машину остаётся.** `review-adversary` и `review-ops` помечены
«держит машину» и идут друг за другом, а не разом: два прохода на одной машине
выдают числа, которые не воспроизведутся. Правило и его причина — дом в
`av-dev:code-review`, раздел «Кто держит машину». Здесь эта цена приемлема:
скилл идёт не на задаче, и часы у него есть.
`review-architecture` и `review-code` машину не держат — уходят первой волной,
разом.
**Задание каждому проходу собирается адресами**: область, дома его тем, контракт
находок, отложенные строки по его теме и признак «вход — область, а не дифф».
Проход, получивший привычное «суди дифф», сузит себя сам.
## Триаж — тот же, вход другой
`review-triage` сводит выводы всех проходов: дедуп по причине, оракул на всё
`critical` и `major`, понижение неподтверждённого до гипотезы, отсев вкусовщины,
ранжирование по ущербу × вероятности.
**Потолка в 7 пунктов здесь нет.** Он существует потому, что отчёт по задаче
читает тот, кто **молча реализует** прочитанное, и длинный список превращается в
разросшийся код. Здесь читатель — человек, и каждый пункт он разбирает вслух.
Вместо потолка — **порядок**: находки идут по убыванию ущерба, и разговор
начинается сверху.
План прогона триажу передаётся составом: перечень проходов и тем. Тема, не
вернувшая отчёта, называется в границах покрытия — правило то же, что в конвейере
задачи.
## Разбор с человеком — главный шаг
**Исход этого скилла — не правки, а согласованный список работ.** Ни одной
находки скилл не чинит сам, даже мелкой: правка по ходу разбора превращает
разговор в работу и съедает то время, ради которого прогон и затевался.
Находки разбираются **по одной, сверху вниз**, и по каждой человек говорит одно
из трёх:
- **берём** — находка становится задачей;
- **не берём** — с причиной; причина уезжает в журнал дефектов `docs/review.md`,
потому что отказ от находки это тоже решение о качестве;
- **не находка** — проход ошибся; это тоже строка журнала, и по ней потом видно,
какой проход даёт ложные срабатывания.
**Показывай находку целиком**, а не заголовком: оракул и последствие — это и есть
то, по чему человек решает. Заголовок без оракула читается как мнение.
**Длинный список разбирается порциями.** Десяток пунктов за раз — потолок
внимания, а не формальность; остальное ждёт следующей порции в том же прогоне.
## Задачи заводит `av-dev:task-track`
**Вызови Skill `av-dev:task-track`** и попроси завести задачи по согласованному
списку — у него на этот вход отдельный сценарий «задачи из ревью и аудита»: своя
нарезка, свой формат, свои правила дублей. Формулировку, оракул и происхождение
находки передавай **дословно**: пересказ теряет как раз оракул, а без него задача
превращается в пожелание.
Заводить записи руками, править индексы или придумывать свой формат нельзя —
мост между скиллами это вызов, а не путь к файлу.
## Запись в журнал ревью
**Прогон оставляет след в `docs/review.md`** — вызовом `av-dev:doc-sync`, который
владеет этим документом. В следе: область, состав проходов, что взято задачами,
что отвергнуто и почему, что проверить было невозможно.
**Второго вопроса здесь не задают, хотя `review.md` — документ рода «новое»**
(`av-dev:doc-sync`, «Два рода правок»): слово по каждой находке человек уже сказал
в разборе, и след цитирует ровно его решения. Правило то же, что у сужения
проверок: спрашивается новое, которое заметил ты, а не то, что человек только что
решил вслух.
Без этого следа второй прогон по той же области начнётся с нуля и предложит те же
находки, от которых человек уже отказался, — а отказ, не оставивший записи,
неотличим от непойманного.
## Доклад
- **область** — что смотрели, адресами;
- **состав прогона** — какие проходы шли, какие темы закрыты, какие нет;
- **находки** — сколько выжило после триажа, сколько взято задачами, сколько
отвергнуто с причиной;
- **заведённые задачи** — слагами, либо строка «каталога задач нет, список
остаётся в докладе»;
- **границы покрытия** — что проверить было невозможно: недоступный инструмент,
неподнимаемая зависимость, область, до которой не дошли;
- **отложенное, которое сняли** — какие строки «отложено» из отчётов ревью
закрыты этим прогоном.
## Тонкости
- **Прогон не правит код** — ни строки. Единственный его артефакт, кроме
разговора, это задачи и запись в журнале ревью.
- **Область меньше — прогон лучше.** Модуль разбирается до конца, сервис целиком
даёт список, который бросают на середине.
- **Находка о процессе — тоже находка.** Пустой список отложенного, дефект,
трижды проскочивший в одном узле, тема без дома — всё это идёт в доклад наравне
с находками о коде.
- **Задачи здесь нет, и границей служит только область.** Дифф, дельта-спеки,
критерии приёмки — всё это про задачу; сюда они не приходят, и подставлять их
«по аналогии» нельзя.
+197
View File
@@ -0,0 +1,197 @@
---
name: code-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` его не открывает и об
его отсутствии молчит. Проект, не ведущий задачи циклом SDD, живёт без 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/code-openspec/scripts/openspec.py"
python3 $os check --dir <корень> # форма config.yaml в проекте
python3 $os form # слепок формы против живого OpenSpec
```
**Копия.** Дом словаря — `shared/axes.md` в репозитории плагина: словарь общий
для всех скриптов, и ни один скилл им не владеет. Правится дом, а не этот файл.
<!-- копия: коды-выхода из av-dev/shared/axes.md -->
**Коды выхода — общий словарь всех скриптов `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:doc-init` — шагом заведения нового проекта, до первого документа;
- `av-dev:canon` в режиме `adopt` — если на переводимом проекте каталога нет
или `config.yaml` остался примером;
- `av-dev:code-resolve` и `av-dev:code-review` — не вызовом по ходу, а отсылкой:
OpenSpec у обоих жёсткая предпосылка, и на проекте без каталога оба посылают
сюда вместо того, чтобы заводить его руками;
- человек — когда конвейер отказался работать без источника требований.
**Копия.** Дом правила — `shared/absence.md` в репозитории плагина.
Правится дом, а не этот файл.
<!-- копия: отсутствие из av-dev/shared/absence.md -->
**Скилл не вправе считать раскладку проекта полной.** Части заводятся порознь и
живут порознь; каждая узнаётся своим следом:
| Чего нет | Как видно | Чего теперь не делает никто |
| --- | --- | --- |
| настройки av-dev | нет `.av-dev.toml` в корне | проект под процесс не заводился; версии нет, настроек нет |
| документы канона | нет `docs/` | проектную конкретику брать неоткуда — темы, инварианты, прецеденты |
| учёт работ | нет каталога задач | запись остаётся владельцу: назови её текстом в докладе |
| источник требований | нет `openspec/config.yaml` | цикл SDD не запускается: спеки не с чем сверять |
**Свой скилл зовётся полным именем** — `av-dev:canon`, `av-dev:task-track`,
`av-dev:code-review`. Короткое имя может разрешиться в устаревшую проектную
копию из `.claude/skills/`, и подмены не будет видно ни в докладе, ни в
поведении.
**Внешний плагин может не стоять.** Их два: `opsx:*` — цикл SDD, и
`av-dev-git:commit` — сообщения коммитов. Путь в дерево чужого плагина не
пишется никогда: `$CLAUDE_PLUGIN_ROOT` ведёт только в своё дерево, а
вычисленный от него путь к соседу либо не откроется, либо откроет чужую
установку. Нужен чужой справочник — зови владеющий им скилл, он прочитает его
сам.
**Отсутствие — исход, а не поломка.** Назови строкой доклада, чего теперь не
делает никто, и продолжай работу. Молчать нельзя: пропуск неотличим от
сделанного. Выдумывать обходной путь нельзя тоже.
**Присутствие узнаётся следом в проекте, а не объявлением.** Перечня того, что
здесь заведено, проект не ведёт — он разошёлся бы с действительностью молча.
<!-- /копия: отсутствие -->
Здесь это значит: документов канона в проекте может не быть, и тогда `context`
называет только те адреса, которые есть, — строкой доклада говорится, что без
паспорта предложение пишут, не зная границы домена.
## Чего этот скилл не делает
- **Не пишет спеки и предложения.** Это `opsx:propose` и конвейер задачи.
- **Не ведёт документы канона** — их дом скилл `av-dev:canon`, и адреса в
`context` только на них ссылаются.
- **Не чинит расхождение формы с версией OpenSpec в проекте.** Оно чинится в
плагине: константы скрипта, образец здесь, запись в журнал версий канона.
- **Не судит, ссылается `context` на документы или пересказывает их.** Машине
этот разрез не виден; его смотрит агент `doc-consistency`. Документов канона в
проекте нет — сверять пересказ не с чем, и так и скажи.
@@ -0,0 +1,101 @@
# Образец `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,400 @@
#!/usr/bin/env python3
"""Форма `openspec/config.yaml`: проверка проекта и сверка слепка с инструментом.
Каталог `openspec/` предпосылка **конвейера**, а не канона документов: без него
не работают ни `opsx:propose`, ни сверка требований конвейером. Поэтому и
проверка формы живёт здесь, рядом со скиллом, который каталог заводит. Раньше она
жила в `docs.py`, у скилла канона, и у файла было два владельца: один заводит,
другой проверяет.
Проверяется то, что **молчит при поломке**. Файл из коробки хуже отсутствующего:
`openspec init` кладёт `config.yaml`, где `context` и `rules` закомментированный
пример на английском. Он есть, он валиден, имя правильное и читается как
настроенный, работая как пустой. Узнаётся это по уже написанному предложению.
Разбираем текстом, а не YAML-парсером: у скриптов ноль внешних зависимостей, а
PyYAML в стандартной библиотеке нет. Всё проверяемое различимо построчно,
комментарии отброшены, ключи верхнего уровня стоят в первой колонке.
Коды выхода общий словарь скриптов av-dev; дом словаря и разбор «дрейф
против окружения» av-dev/shared/axes.md. Значения в константах ниже.
"""
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:canon"
)
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())
+552
View File
@@ -0,0 +1,552 @@
---
name: code-resolve
description: "Взять одну задачу и довести её до закрытия. Одна точка входа, три сценария, и выбирает сценарий сам скилл, прочитав постановку. Способ известен и меняется поведение — сценарий решения: цикл Spec Driven Development (opsx propose → чекпоинт с объяснением человеческим языком, где форму решения одобряет человек → opsx apply → ревью кода постоянным составом → archive и отражение в документах молча → одна реплика о новом, где человек решает, что заводится: ADR, конвенция, задачи из урожая ревью → письмо одобренного → коммит → закрытие). Плановых стопов у сценария два, и оба про решения человека: чекпоинт до кода решает форму решения, реплика после кода — что из найденного переживёт задачу. Способ известен, а спека не меняется (тип chore: тулчейн, зависимости, сборка, гит-хуки, перенос, чистка) — сценарий обслуживания: правка → гейт со сверкой состава проверок → ревью фиксированным планом без change (autotests, operations, плюс conventions и техника, если тронут код) → синк документации, где почти всё письмо — отражение фактов и идёт молча → коммит → закрытие; планового стопа нет, change не заводится. Нашлась дельта-спека — задача оказалась шире своего типа: стоп с объяснением простым языком и двумя решениями человека, переформулировать запись в fix или feature и решать её процессом того типа следующим прогоном либо прекратить работу. Способа нет, постановка мутная, тип research — сценарий разведки: вопрос и рамки → чтение документов, кода и внешних источников (можно opsx:explore) → чекпоинт вариантов: 2–4 способа решить, цена каждого, что становится невозможным, рекомендация → ответ уезжает в документы канона, исход — в задачи → вычитка написанного → коммит → закрытие. Разведка кода не пишет и change не заводит, а выбранный способ реализуется следующим прогоном. На входе путь к файлу задачи, её слаг или просто текст постановки: размеченная запись не обязательна — текст берётся так же, как его берёт opsx:propose, и текстом идут все три сценария. Использовать, когда просят взять, сделать или решить задачу — хоть записью из каталога, хоть описанием прямо в разговоре, — обновить зависимости или сборку, разобраться, изучить, сравнить подходы, проработать сырую идею, ответить на вопрос из беклога."
---
# Работа над одной задачей
Проводит **одну** задачу от постановки до закрытия. Вокруг планового стопа — без
согласований: механику не обсуждаем, делаем.
**Сценария три, а точка входа одна.** Какой из них идёт, решает **скилл**,
прочитав постановку, а не человек до вызова: «есть ли у задачи очевидный способ
решения» и «меняется ли то, что записано в спеке» видно после чтения записи, и
требовать этих суждений от вызывающего значит требовать их раньше, чем они
возможны.
| Сценарий | Когда | Чем кончается |
| --- | --- | --- |
| **решение** | способ известен, меняется поведение | код, ревью, архив, коммит, закрытие |
| **обслуживание** | способ известен, спека не меняется: тип `chore` | правка, ревью, синк, коммит, закрытие |
| **разведка** | способа нет: тип `research`, сырая идея, мутная постановка | ответ в документах, задачи, коммит, закрытие |
Ход каждого сценария живёт своим справочником: **решение**
[references/solve.md](references/solve.md), **обслуживание**
[references/maintain.md](references/maintain.md), **разведка**
[references/research.md](references/research.md). Здесь только общее: вход,
развилка, правила, которые не зависят от сценария. Сценарии лежат порознь и
одинаково, потому что привилегированного среди них нет: тот, что жил бы прямо
здесь, читался бы как основной, а прочие — как оговорка.
## Предпосылки
- **OpenSpec и скиллы `opsx:*` — жёсткая предпосылка сценария решения**, а не
опция. На них стоят его шаги 2, 4 и 6 и проход `review-specs`
(они завязаны на `openspec/changes/<id>/specs/*/spec.md` и на
`openspec validate --strict`). **Проект без OpenSpec этим скиллом не ведётся**
подключай OpenSpec, а не вырождай цикл сценария; почему ветка деградации здесь
не
пишется, сказано в `av-dev:code-review`, раздел «Предпосылки», и дом у этого
довода там. Заводить руками не надо: каталог и настройку в `config.yaml`
делает скилл `av-dev:code-openspec`. **Сценариям разведки и обслуживания
OpenSpec не нужен** — они не заводят change; `opsx:explore` берётся разведкой,
если плагин есть.
- **Проектные копии этих скиллов и агентов удаляются при установке плагина.**
<!-- копия: проектные-копии из README.md -->
Голые имена в `.claude/skills/`: `resolve`, `review`, а у проектов прошлого
поколения ещё `task-pipeline`, `review-pipeline`, `task-batch`. С префиксом
проекта: `<проект>-task-pipeline`, `<проект>-review-pipeline`. Агенты:
`.claude/agents/<проект>-review-*.md`.
Две копии одного скилла расходятся, и побеждает та, что **короче названа**:
короткое имя разрешится в устаревшую проектную копию молча и без признаков
подмены.
<!-- /копия: проектные-копии -->
### Чего может не быть
**Копия.** Дом правила — `shared/absence.md` в репозитории плагина: правило
общее для всех, кто приходит в чужой проект, и ни один скилл им не владеет.
Правится дом, а не этот файл.
<!-- копия: отсутствие из av-dev/shared/absence.md -->
**Скилл не вправе считать раскладку проекта полной.** Части заводятся порознь и
живут порознь; каждая узнаётся своим следом:
| Чего нет | Как видно | Чего теперь не делает никто |
| --- | --- | --- |
| настройки av-dev | нет `.av-dev.toml` в корне | проект под процесс не заводился; версии нет, настроек нет |
| документы канона | нет `docs/` | проектную конкретику брать неоткуда — темы, инварианты, прецеденты |
| учёт работ | нет каталога задач | запись остаётся владельцу: назови её текстом в докладе |
| источник требований | нет `openspec/config.yaml` | цикл SDD не запускается: спеки не с чем сверять |
**Свой скилл зовётся полным именем** — `av-dev:canon`, `av-dev:task-track`,
`av-dev:code-review`. Короткое имя может разрешиться в устаревшую проектную
копию из `.claude/skills/`, и подмены не будет видно ни в докладе, ни в
поведении.
**Внешний плагин может не стоять.** Их два: `opsx:*` — цикл SDD, и
`av-dev-git:commit` — сообщения коммитов. Путь в дерево чужого плагина не
пишется никогда: `$CLAUDE_PLUGIN_ROOT` ведёт только в своё дерево, а
вычисленный от него путь к соседу либо не откроется, либо откроет чужую
установку. Нужен чужой справочник — зови владеющий им скилл, он прочитает его
сам.
**Отсутствие — исход, а не поломка.** Назови строкой доклада, чего теперь не
делает никто, и продолжай работу. Молчать нельзя: пропуск неотличим от
сделанного. Выдумывать обходной путь нельзя тоже.
**Присутствие узнаётся следом в проекте, а не объявлением.** Перечня того, что
здесь заведено, проект не ведёт — он разошёлся бы с действительностью молча.
<!-- /копия: отсутствие -->
Скилл зовёт `av-dev:code-review`, `av-dev:doc-sync` и `av-dev:task-track`
все трое в этом же плагине и разрешаются всегда. Чем оборачивается отсутствие
части раскладки, под которую они работают, сказано на самих шагах сценариев.
Перед стартом прочитай `CLAUDE.md` проекта и то, на что он ссылается, если ещё
не в контексте. Проектные факты, нужные ревью — инварианты, семантика гейта,
объёмы, модель угроз, прецеденты, — живут в **документах канона**;
карта «что где» — `references/project-facts.md` конвейера ревью.
**Документов канона нет — проект к нему не приведён.** Скажи это строкой и
предложи скилл `av-dev:canon`: одна операция на проект против поразрядной
деградации на каждой задаче. Работу при этом не останавливай.
## Вход
Задача задаётся **путём к файлу, именем файла, слагом или просто текстом**.
Ничего из этого не задано — попроси у вызывающего и остановись; сам в беклог не
лезь и приоритеты не интерпретируй: что делать дальше, решает не этот скилл.
**Форм постановки две, и обе полноправны:** запись каталога задач и текст,
переданный вызовом. Форма — не сценарий: развилка ниже у них общая, и текст
принимают все три сценария.
### Запись из каталога
**Запись сперва проверяется на готовность, и проверяет её машина.**
Вызови Skill `av-dev:task-track` и попроси прогнать `ready <слаг>`: он смотрит
тип, пустой ли раздел вопросов и собраны ли разделы схемы типа. Судить это глазами нельзя — ровно тот случай, где машина дешевле и точнее,
а цена ошибки отложенная: недостающие критерии приёмки обнаружатся на приёмке,
когда сверять уже не с чем.
**`ready` отказал — это исход, а не препятствие для тебя.** Скажи, чего не
хватает, и остановись: дописывать чужую запись за автора не твоя работа. Исход —
«не доведена», с названной причиной.
**Отказ `ready` сценарий не выбирает.** Запись `research` без раздела «Вопрос»
(сырьё) — отказ и здесь: у неё нет вопроса, и разведывать нечего.
Каталога задач в проекте нет — прогонять нечего, и постановка приходит текстом
по построению: дальше по разделу ниже.
### Постановка текстом
**Текст — вход, а не урезанный режим.** Ровно так берёт постановку
`opsx:propose`: предложение делается из фразы человека, а не из заранее
размеченной записи. Требовать записи там, где работа уместилась в разговор,
значит заводить учёт ради учёта — след у прогона остаётся и без неё: коммит, а у
решения ещё и заархивированный change.
**Первой репликой покажи, как ты понял постановку** — рядом с названным
сценарием, одной-двумя фразами: что считаешь предметом работы и где проводишь
границу. Запись толкуется по разделам, текст — молча, и расходится он с замыслом
ровно там, где его никто не показал. Человек, написавший текст, сидит в этом же
разговоре и поправляет одной фразой; автора записи, написанной месяц назад,
рядом нет, и потому текстовая постановка проверяется дешевле, а не хуже.
Что несёт запись и чем это заменяется, когда её нет:
| Что несёт запись | Чем заменяется у текста |
| --- | --- |
| готовность, проверенную машиной | читаешь постановку сам и говоришь строкой, что `ready` не гонялся |
| тип, объявленный автором | тип называешь ты — вслух, первой репликой, вместе со сценарием |
| критерии приёмки с оракулами | те, что есть в тексте; недостающие [решение](references/solve.md) добирает на чекпоинте, [обслуживание](references/maintain.md) объявляет строкой отсутствующими |
| адрес, куда ляжет ответ разведки | назначаешь сам и по канону, а не по удобству — [research.md](references/research.md), шаг 1 |
| закрытие как след работы | закрывать нечего, и шаг закрытия отпадает вместе с записью |
**Записи в каталог этот скилл не заводит — ни перед работой, ни задним числом
ради закрытия.** Граница «беклогом не владеет» действует и здесь. Работа не
уместилась в прогон, её надо ставить в очередь или из неё выросла пачка — скажи
это строкой и предложи `av-dev:task-track`: заводит он и по своим правилам.
**Похожую запись в беклоге не ищешь.** Человек назвал работу текстом — значит,
предмет прогона этот текст, а не строка индекса, которая на него похожа.
Наткнулся на такую строку по ходу — скажи о ней строкой доклада и не закрывай:
закрытие записи это приёмка, и поручали её не тебе.
## Развилка: какой сценарий
Сценарий — ось процесса; перечень осей и их границ —
[shared/axes.md](../../shared/axes.md).
Она в два вопроса, и оба стоят до всякой работы.
**Первый: есть ли у задачи один очевидный способ решения?**
- **нет** — тип `research`, сырая идея, новое и незнакомое, мутная постановка,
два подхода с разной ценой. **Сценарий разведки**
[references/research.md](references/research.md);
- **есть** — что делать, понятно; спорно только как. Тогда второй вопрос.
**Второй: меняется ли то, что записано в `openspec/specs/`?**
- **меняется** — появляется или правится поведение. **Сценарий решения**
[references/solve.md](references/solve.md);
- **не меняется** — тулчейн и сборка, зависимости, гит-хуки и шаги гейта,
перенос, чистка. **Сценарий обслуживания**
[references/maintain.md](references/maintain.md).
**Второй вопрос решается связкой из двух признаков, и оба обязательны:** тип
записи предлагает (`chore`, реже `fix`, возвращающий поведение к уже
записанному), а отсутствие дельт подтверждает. Тип объявляет автор и может
ошибиться; отсутствие дельт — твоё суждение и принимается только вместе с типом.
Признаки разошлись — это стоп, а не выбор: скажи, что тип и предмет работы не
сходятся, и остановись. Подробно — [maintain.md](references/maintain.md), раздел
«Признак — связка, а не одно условие».
**Признак не в объёме работы, и это относится к обоим вопросам.** Крупная задача
с очевидным способом идёт в решение; маленькая, но незнакомая — в разведку;
однострочная правка, меняющая поведение, идёт полным циклом решения, а не
обслуживанием. Путь, выбираемый по самооценке размера, — самый дешёвый способ
«ускориться» и самый дорогой по последствиям. Тип `research` в разведку идёт
всегда: её исход знание, а не изменение системы.
**Назови выбранный сценарий вслух первой репликой** — одной строкой, с причиной.
Молча выбранный сценарий человек обнаруживает по тому, что работа пошла не туда,
и обнаруживает поздно.
### Сценарий выбирается один раз
**Смена сценария по ходу — событие, а не тихий поворот**, и каждая смена
устроена по-своему:
- **решение → разведка**: обнаружилось, что очевидного способа нет. Это **стоп**
с исходом «нужна разведка»: назови, что именно неясно, и не продолжай.
Кода к этому моменту не написано, и писать его «пока разбираемся» нельзя;
- **обслуживание → решение**: нашлась дельта-спека, то есть поведение всё-таки
меняется. Задача не сломалась — она **оказалась шире своего типа**, и стоп
здесь несёт человеку выбор: назови тип, которым она оказалась (`fix`
расходится с заявленным, `feature` — снаружи появляется то, чего не было),
объясни простым языком, что нашлось, и дай два решения — **переформулировать
запись и решать процессом того типа следующим прогоном** либо **прекратить
работу**. Третьего — «доделать как обслуживание» — нет. Исход в обоих случаях
«меняется спека»; сделанное остаётся в рабочем дереве незакоммиченным, тип
меняет `av-dev:task-track` и только после ответа. Подробно —
[maintain.md](references/maintain.md), раздел «Дельта нашлась по ходу»;
- **обслуживание → разведка**: форма правки неизвестна (мажорное обновление,
смена сборщика). **Стоп** с исходом «нужна разведка», по тому же основанию,
что и у решения;
- **разведка → решение или обслуживание**: способ выбран на чекпоинте вариантов.
Разведка **всё равно доводится до конца** — ответ записан, задачи уточнены,
коммит сделан, — и работа идёт **следующим прогоном**, который запускает
человек.
Обратной смены «решение → обслуживание» нет: задача, заведшая change, доводится
циклом решения. Дельта-спеки, оказавшиеся пустыми, — повод назвать это на
чекпоинте, а не свернуть на короткий путь из середины длинного.
**Соблазн «разведаю по ходу» живёт именно здесь**, и он дорог тем, что выглядит
экономией одного прогона. Разведка внутри решения не имеет своего чекпоинта:
выбор делается тем, кто уже начал писать, и человек видит его только в
объяснении, где обсуждать выбор поздно. Ровно за это сценарии и разведены — не за
то, что это разные работы, а за то, что у них разные моменты для человека.
```mermaid
flowchart TD
in["вход: файл, слаг или текст"]
form{"форма постановки"}
ready["ready: готовность записи<br/>av-dev:task-track"]
plain["понимание, тип и границы —<br/>первой репликой; ready не гонится,<br/>закрывать потом нечего"]
fork{"есть очевидный<br/>способ решения?"}
fork2{"меняется ли<br/>спека?"}
solve["сценарий решения<br/>references/solve.md<br/>код, ревью, архив, коммит"]
main["сценарий обслуживания<br/>references/maintain.md<br/>правка, ревью, синк, коммит"]
res["сценарий разведки<br/>references/research.md<br/>ответ в документы и задачи"]
in --> form
form -->|"запись каталога"| ready --> fork
form -->|"текст"| plain --> fork
fork -->|"да"| fork2
fork -->|"нет"| res
fork2 -->|"да"| solve
fork2 -->|"нет: тип chore"| main
solve -.->|"способа всё же нет:<br/>стоп, кода не написано"| res
main -.->|"нашлась дельта: стоп,<br/>тип на fix или feature,<br/>следующим прогоном"| solve
main -.->|"форма неизвестна:<br/>стоп"| res
res -.->|"способ выбран:<br/>следующим прогоном,<br/>зовёт человек"| solve
```
Схема — **сводка**: содержание сценариев в их справочниках, и при расхождении
прав справочник.
## Кто пишет: письмо уходит агентам
**Своими руками этот скилл не пишет ничего** — ни спек, ни кода, ни правок по
находкам ревью. Каждую такую работу выполняет **отдельный агент**: оркестратор
ставит задание и читает возврат. Дальше эта работа зовётся **письмом** — всё, что
скилл написал бы сам, если бы писал.
Причина про контекст, и она одна. Оркестратор ведёт задачу целиком: выбрал
сценарий, собирает чекпоинт, сверяет перечень тем с исходом, пишет доклад, — а
для всего этого надо помнить постановку, критерии приёмки и то, что человек
одобрил. Письмо тянет в контекст ровно то, чего в докладе не будет ни строкой:
содержимое тронутых файлов, вывод линтеров и гейта, перебранные варианты правки.
Забитый этим контекст теряет одобренное и постановку — и теряет **молча**: доклад
остаётся связным, а сверять его уже не с чем.
**Разведённость тут ни при чём**, и путать два довода нельзя. Агент пишет по
заданию оркестратора и отвечает перед ним; разведён с автором тот, кто работу
**судит**, — проходы ревью.
| Работа | Где шаг |
| --- | --- |
| предложение и дельта-спеки — `opsx:propose` | [solve](references/solve.md), шаг 2 |
| правки спек и дизайна по сказанному на чекпоинте | [solve](references/solve.md), шаг 3 |
| код — `opsx:apply`, вместе с гейтом до зелёного и поведенческой верификацией | [solve](references/solve.md), шаг 4 |
| правки по находкам триажа, помеченным `инлайн` | [solve](references/solve.md), шаг 5; [maintain](references/maintain.md), шаг 4 |
| правка оснастки в сценарии обслуживания | [maintain](references/maintain.md), шаг 2 |
| архивация change и отражение в документах — `opsx:archive` и `av-dev:doc-sync` | [solve](references/solve.md), шаг 6, такт 1; [maintain](references/maintain.md), шаг 5 |
| письмо одобренного нового в документы канона | [solve](references/solve.md), шаг 6, такт 3; [maintain](references/maintain.md), шаг 5 |
**Остальное остаётся оркестратору, и перечень закрыт:** выбор сценария и стопы,
чекпоинт, вызовы `av-dev:code-review`, `av-dev-git:commit` и `av-dev:task-track`,
сверка плана с исходом, урожай и доклад. **Коммит и закрытие задачи агенту не
отдаются ни в одном сценарии** — они необратимы для учёта: закрытие удаляет запись
и правит индексы, а коммит уезжает в историю. Оркестратор делает их сам, уже
сверив перечень тем с исходом. Ни одна из этих
работ не пишет файлов проекта — они и есть та работа, ради которой контекст
берегут.
**Разведка сюда не попадает вовсе.** Её письмо — записка в документы канона и
записи задач, то есть тот самый текст, из которого собираются чекпоинт вариантов
и доклад. Отдать его агенту значило бы получить обратно пересказом то, что
и так надо держать целиком.
### Устава у этих агентов нет
Проходы ревью ходят уставами (`av-dev/agents/`), потому что уставом задаётся
**суждение**: что искать и что считать находкой. Здесь суждения нет — работа
нормирована скиллами `opsx:*`, конвенциями проекта и находками триажа, а устав
стал бы вторым домом того же и разошёлся бы с ним молча. Зовётся агент общего
назначения, и всё, чем один его прогон отличается от другого, приходит заданием.
### Задание собирается адресами
**Агент не видел разговора.** Он не знает ни постановки, ни выбранного сценария,
ни того, что уже одобрено на чекпоинте. Поэтому задание самодостаточно, а вещи в
нём называются **адресами, а не пересказом** — по тому же правилу, по которому
проход ревью получает дом темы путём и разделом. В задании:
- корень проекта, текущая ветка и база диффа. Ветку агент не создаёт и не
переключает, не пушит — правило то же, что у скилла;
- **что делать**: файл задачи либо её текст дословно, критерии приёмки,
идентификатор change;
- **что читать**: `CLAUDE.md`, конвенции проекта, дельта-спеки change;
- **находки — дословно**, как их вернул триаж, вместе с
оракулом;
- **границы**: правится названное, соседнее не улучшается заодно; развилок агент
не решает, задач не заводит, ничего не коммитит и наружу не ходит — правило
необратимого действует и на него (раздел «Когда спрашивать вне чекпоинта»);
- **чем кончает**: гейт зелёный, а если задача меняет наблюдаемое поведение —
прогнана поведенческая верификация.
**Пересказ находки — самая дорогая экономия из возможных.** Находка триажа несёт
оракул, и пересказ теряет как раз его: агент чинит то, что понял, гейт зеленеет,
а в отчёт уезжает «исправлено».
### Возврат — не длиннее экрана
Агент возвращает: что сделано, **адресами** тронутого; исход гейта, чем он
прогнан, где логи шагов и **отпечаток дерева сразу после прогона**; что не
удалось и почему; вопросы, если по заданию их не разрешить. Отпечаток нужен
ревью: по нему ступень автотестов засчитывает этот прогон вместо своего
(`av-dev:code-review`, ступень 1) — без него гейт гоняется дважды на том же
дереве.
Диффа, пересказа кода и логов в возврате нет — иначе экономия, ради которой шаг
и вынесен, отменяется в момент возврата.
**Чек-лист синка — единственное исключение из «не длиннее экрана».** Он приходит
из хвостового агента целиком и целиком уезжает в доклад: тронутые документы
поимённо, предложенное — строкой с основанием, нетронутые — одной строкой с общей
причиной. Сжать его своими словами значит потерять принуждённое отрицание, ради
которого шаг и существует.
**Предложения из этого чек-листа оркестратор не исполняет сам.** Они уезжают в
реплику человеку вместе с урожаем ревью, и написанным становится только то, что
он назвал (`solve.md`, шаг 6, такт второй). Агент, вернувший предложение, свою
работу сделал — заведение нового не его решение и не твоё.
**Возврату на слово не верят, и перечитывать за агентом дифф для этого не надо.**
Верят независимым артефактам: зелёному гейту, отчёту триажа, ревью следующего
шага. Своей прозе здесь верить нельзя ровно по той причине, по которой ей не
верит конвейер ревью, — её написал тот, кто мог и пропустить.
**Артефакты, написанные для человека, оркестратор читает сам**: `proposal.md` и
`design.md` нужны ему на чекпоинте. Это не переполнение контекста, а его работа.
### Один агент на шаг, а не на файл
Нарезка по файлам разводит одну правку по разным контекстам, и сходиться она
будет в гейте, то есть после. Повторный проход того же шага — **новое задание**,
а не продолжение прежнего: агент прежнего не помнит, и рассчитывать на его память
нельзя.
**Правило про контекст, а не про полномочия.** Агент упал, вернул не то или не
понял задания — повтори задание, дописав то, чего в нём не хватило. Не вышло и во
второй раз — делай сам и **скажи это строкой доклада**: прогон стоил дороже, чем
должен, и это факт для человека, а не стоп.
## Автономность и плановые стопы
**Стопов у сценария не больше двух, и каждый — про решение человека, а не про
ход работ.**
| Сценарий | Стоп до письма | Стоп после письма |
| --- | --- | --- |
| решение | чекпоинт: объяснение после предложения и до кода | реплика шага 6: что из найденного заводится |
| разведка | чекпоинт вариантов до первого написанного требования | — исход и так уезжает в документы по выбранному варианту |
| обслуживание | — планового нет | реплика шага 5, и только если появилось новое |
**Второй стоп короче первого и часто не случается вовсе.** Первый решает форму
решения, и без ответа работа не идёт дальше; второй решает, что из найденного
переживёт задачу, и при пустом списке нового его просто нет. Правило вокруг обоих
общее.
**У обслуживания планового стопа нет вовсе, и это следствие, а не поблажка.**
Один стоп с ожиданием ответа у него всё же есть — по найденной дельта-спеке, — но
плановым он не является: через него проходят только те прогоны, где задача
оказалась не тем, чем объявлена.
Чекпоинт объясняет человеку **выбор**, а у обслуживания выбора нет по
построению: что делать, сказано в записи, и объяснение свелось бы к пересказу
задачи её же автору. Стоп, на котором нечего решать, вырождается в обряд
одобрения и обесценивает те стопы, где решать есть что. Правило необратимого
(ниже) действует там полностью и срабатывает чаще, чем в двух других сценариях:
выкладка, токены, хуки и чужие данные — обычное содержимое задач обслуживания.
**Вокруг чекпоинта умолчание прежнее — делать, а не спрашивать.** Чекпоинт не
отменяет автономность, он даёт развилкам плановое место, куда копиться.
Разрез простой:
- развилка найдена **до** чекпоинта — она его и ждёт. Не спрашивай отдельно:
чекпоинт рядом и стоит дёшево, а три вопроса подряд стоят дороже одного
разговора;
- развилка найдена **после** чекпоинта — старое правило: **запиши вопрос и доведи
остаток**, не останавливаясь.
Запись вопроса устроена так:
1. **Запиши там, где проект держит вопросы** (секция беклога, файл задачи,
трекер — это знает проект). Проект не сказал, куда, — отдельной секцией
`Вопросы` в своём докладе, и это тоже исход. Тело отвечает на три вещи: **что
именно решить**, **какие есть варианты и цена каждого**, **что стоит, пока
решения нет**. Плюс твоя рекомендация — человек чаще соглашается, чем выбирает
заново, и готовое суждение экономит ему весь контекст.
2. **Переформулируй задачу на остаток** — то, что делается без этого решения.
Назови границу: докуда доводим сейчас.
3. Доведи остаток до конца и закоммить. Задача не «висит на вопросе», она сделана
в объявленных границах. В разведке остаток — это ответ в объявленных рамках:
что успели узнать, где остановились и почему.
**Что остатком не является — правило живёт не здесь.** Канонический текст с обеими
оговорками — в скилле `av-dev:task-groom`, раздел
`## Вопрос, блокер, необратимое`, подраздел «Отличать вопрос от застревания».
Правило принадлежит управлению задачами, потому что решает **сделана задача или
вышла**, — это исход планирования, а не исполнения. **Ссылайся, не
пересказывай:** копия, заведённая здесь, уже однажды разошлась с оригиналом и
потеряла из перечня самое необратимое — запись **наружу**.
Коротко, чтобы знать, когда идти читать: остаток проверяется двумя порогами —
**материализация нерешённого** (запись состояния, зависящего от неотвеченного
вопроса) и **пол по пользе** (из остатка пропала польза, названная в постановке).
Оба порога — стоп: первый поднимает решение до начала записи, второй даёт исход
«не доведена».
Каталога задач в проекте нет — правило не отменяется, а становится
осторожнее: прежде чем записать зависящее от нерешённого куда бы то ни было —
в хранилище, в журнал, в витрину или наружу, — спрашивай человека.
Нет полезного остатка — задача заканчивается исходом «не доведена», вопрос
записан, ничего не коммитится наполовину.
### Когда спрашивать вне чекпоинта
По другому основанию — не «сложное решение», а **необратимое действие**:
- деплой, выкладка наружу, смена публичного адреса или токенов;
- удаление или перезапись рабочих данных, включая подрезку архивов;
- всё, что уходит за пределы машины.
Здесь ошибка не откатывается коммитом, поэтому спрашиваем даже когда решение
кажется очевидным.
## Границы: чем этот скилл не владеет
- **Беклогом и порядком работ.** Задача приходит извне. Скилл её не выбирает,
не переставляет, не заводит и не переоценивает.
- **Форматом задач и документов.** Индексы и документы канона руками не правятся,
путь к чужому скрипту не выдумывается: этим владеют `av-dev:task-track` и
`av-dev:doc-sync`. Закрытие — работа этого скилла, и это осознанное решение с
названной ценой: **приёмщик и исполнитель совпали**. Закрытие поэтому **не окончательно** — человек
возвращает задачу `reopen` с причиной (на доработке это делают грумингом,
`av-dev:task-groom`, на стройке — сразу, как заметили), а доклад
по критериям приёмки становится единственным, по чему приёмка вообще возможна.
- **Определением ценности.** «Нужна ли эта функциональность» — не вопрос этого
скилла ни на одном шаге и ни в одном сценарии. Чекпоинт решения спрашивает «так
ли решаем», чекпоинт разведки — «каким из способов», но не «надо ли».
Что не принадлежит **отдельному сценарию**, названо у него же: урожай ревью и
выбор способа — в [solve.md](references/solve.md), изменение поведения и нарезка
пачки — в [maintain.md](references/maintain.md), код и приоритет — в
[research.md](references/research.md).
## Наблюдаемые исходы
**У каждого сценария их четыре**, и живут они у сценария:
[решение](references/solve.md) — сделана, не доведена, оказалась крупнее задачи,
нужна разведка; [обслуживание](references/maintain.md) — сделана, не доведена,
меняется спека, нужна разведка; [разведка](references/research.md) — способ
выбран, знание записано, отказ, не доведена.
Общего исхода нет намеренно. «Сделана» у решения и «знание записано» у разведки —
разные вещи с разной приёмкой, и слово, накрывающее оба, скрывало бы именно то,
чем прогон кончился. «Сделана» у решения и у обслуживания совпадает словом, но не
определением: у первого в него входит пройденный чекпоинт и заархивированный
change, у второго — сверенный состав гейта и синк.
## Доклад
Ядро общее, и в нём обязательно:
- **какой сценарий шёл** — решение, обслуживание или разведка, — и почему выбран
он;
- **исход** одним из четырёх слов своего сценария и, если он не благополучный,
чем ограничен результат;
- **постановка пришла текстом** — сказать это прямо: как она понята, что `ready`
не гонялся и что закрывать было нечего;
- что сделано, какие вопросы записаны и куда;
- **шаг письма, сделанный не агентом, а тобой** — с причиной: раздел «Кто пишет»
требует называть это строкой, а не молча;
- чего проверить или узнать **не удалось**.
Сверх ядра каждый сценарий добавляет своё: [solve.md](references/solve.md) —
чекпоинт, change, критерии приёмки, урожай и границы покрытия;
[maintain.md](references/maintain.md) — чем подтверждён признак, состав гейта до
и после, критерии приёмки, урожай и границы покрытия;
[research.md](references/research.md) — вопрос и ответ, адреса записи, заведённые
задачи, рамки.
## Тонкости
- **Не завязывайся на основную ветку и корень репозитория.** Скилл работает в
текущем worktree и на текущей ветке: не делай `git checkout`/`switch`, не
создавай веток, не пушь.
- Прогон проходит **не больше одного** чекпоинта, и это норма, а не упрощение.
Два чекпоинта за одну задачу — цена незнания способа, и платится она двумя
прогонами, а не одним длинным. У обслуживания чекпоинта нет ни одного, и это
тоже норма: там нечего решать. Реплика о новом чекпоинтом не является и этого
счёта не касается — она решает не форму решения, а судьбу находок.
- Держи вызывающего в цикле короткими репликами на переходах фаз, но не проси
подтверждать механику. Мест, где **ждут ответа**, ровно два, и оба названы в
«Автономности»: чекпоинт до кода и реплика о новом после него. Третьего нет ни
в одном сценарии, и заводить его нельзя.
- **Сценарий назван вслух — значит, его можно оспорить.** Человек, увидевший в
первой реплике «иду разведкой, потому что способа не видно», поправит выбор
одной фразой; молча выбранный сценарий он поправит через полчаса работы.
@@ -0,0 +1,520 @@
# Сценарий «обслуживание»
Способ решения известен, а **того, что нормирует спека, задача не трогает**:
тулчейн и сборка, зависимости, гит-хуки и шаги гейта, перенос и чистка. Сценарий
**пишет код**, но не заводит change и не пишет требований. Исход — работающая
оснастка и синхронная ей документация.
Сценарий выбирается развилкой на входе скилла ([SKILL.md](../SKILL.md), раздел
«Развилка: какой сценарий») и называется вслух первой репликой. Здесь только его
ход; общее для всех трёх сценариев — вход, чего может не быть, правило
записанного вопроса, правило необратимого — живёт в SKILL.md и тут не
пересказывается.
## Почему цикл SDD здесь не урезан, а остался без входа
Это не поблажка по цене, и называть сценарий «коротким путём для мелких задач»
нельзя: путь, выбираемый по самооценке размера, и есть тот самый дешёвый способ
«ускориться», против которого написана вся защита сценария решения.
**У обслуживания нет дельта-спек по построению.** Тип `chore` определён через
«наблюдаемое поведение не меняется», а на дельтах стоит весь цикл: `propose`
их порождает, `review-specs` сверяет **с ними**, объяснение чекпоинта собирается
из `proposal.md` и `design.md`, `archive` вливает их в актуальные спеки. Change
без дельт — пустой артефакт, который потом надо архивировать.
Шаги цикла здесь не пропущены — **им нечего обрабатывать**. Отсюда и состав
сценария: выпали ровно те шаги, у которых нет предмета, и не выпал ни один из
тех, у которых он есть.
## Признак — связка, а не одно условие
Сценарий выбирается двумя проверками сразу, и обе обязательны:
1. **тип записи предлагает**`chore`, реже `fix`, чьё исправление возвращает
поведение к уже записанному в спеке;
2. **отсутствие дельт подтверждает** — прочитав постановку, ты не находишь
требования, которое пришлось бы добавить, изменить или снять.
Один признак без второго не выбирает сценарий. Тип объявляет автор записи, и он
может ошибиться в обе стороны; отсутствие дельт — суждение исполнителя, и оно
принимается только тогда, когда согласуется с объявленным типом. Расхождение
двух признаков — это не развилка, а стоп: скажи, что тип и предмет работы
разошлись, и остановись.
**Имя сценария не равно имени типа, и это намеренно.** `fix` без дельта-спеки
идёт сюда законно — поведение разошлось с **заявленным**, значит заявленное уже
записано, и менять спеку не нужно. Сценарий, названный именем типа, такую задачу
либо отправил бы в полный цикл ради пустого change, либо принял бы как
исключение, а исключения не исполняются.
### Постановка текстом — тип называешь ты, и называешь вслух
Первый признак приходит от автора записи; **текст типа не несёт** (SKILL.md,
«Постановка текстом»). Оба признака тогда твои, и связка выродилась бы в одно
суждение — то самое, ради разведения которого она и заведена.
Разведённость здесь восстанавливается местом, а не вторым автором: **тип и
предмет работы называются до начала работы, первой репликой** — «иду
обслуживанием: считаю это `chore`, потому что …; спека не меняется, потому что
…». Человек, написавший текст, читает это раньше первой правки и поправляет
одной фразой. Названный **после** работы тип не признак, а объяснение уже
сделанного: к этому моменту у тебя есть готовый дифф, и он всегда подтверждает
тот тип, под который писался.
Не назвал — признака нет вовсе, и сценарий выбрал сам себя. Это ровно тот
случай, где «самый частый способ соврать этим сценарием» (раздел «Тонкости»)
ничего не стоит: автора, чей тип можно было бы опровергнуть, здесь нет.
## Дельта нашлась по ходу — стоп, и у него свой порядок
Признак тот же, что у отработки ревью в решении (шаг 5): **меняется ли то, что записано в
`openspec/specs/`**. Обнаружилось, что меняется, — работа перестала быть
обслуживанием в ту же секунду, и продолжать её нельзя: коммит обслуживания
заявляет «поведение не менялось», а оно меняется.
**Задача при этом не сломалась — она оказалась шире своего типа.** Поэтому стоп
здесь не «бросить и доложить», а три шага по порядку.
**1. Назови тип, которым задача оказалась.** Разрез тот же, по которому типы и
разведены:
- **`fix`** — поведение расходится с **заявленным**: спека уже описывает верное,
и правка возвращает систему к записанному;
- **`feature`** — снаружи появляется то, чего не было: спеке нужно новое
требование.
Тип называется прямо и с причиной. «Нужно менять спеки» без имени типа
перекладывает классификацию на человека в тот момент, когда весь материал для неё
у тебя.
**2. Объясни человеку простым языком.** Экран текста, не больше:
- **что просили сделать** — одной фразой из записи;
- **что нашлось** — какое поведение меняется, словами домена, а не именами
файлов и функций;
- **почему это перестало быть обслуживанием** — одной фразой: у обслуживания
поведение не меняется по определению;
- **чем задача становится**`fix` или `feature`, с причиной из разреза выше;
- **что уже сделано** и что из этого лежит в рабочем дереве.
Проверка на простой язык — общая у трёх сценариев:
<!-- копия: чекпоинт-простой-язык из av-dev/skills/code-resolve/references/solve.md -->
**в тексте нет `SHALL`, нет имён файлов и функций там, где вещь называется
по-русски, и нет слов, которых нет в паспорте проекта.**
<!-- /копия: чекпоинт-простой-язык -->
**3. Дай два решения и жди ответа.** Их ровно два, и оба законны:
- **переформулировать запись** — тип меняется на названный, и дальше задача идёт
**процессом своего типа**: сценарием решения, следующим прогоном. Формат записи
правит `av-dev:task-track`, а не ты: у нового типа своя схема разделов, и
готовность её проверит `ready` — той же машиной, что и на входе. Прогон
обслуживания на этом кончается, исход — «меняется спека».
**Постановка пришла текстом — переформулировать нечего:** человек либо
запускает следующий прогон тем же текстом, и он пойдёт решением, либо заводит
запись через `av-dev:task-track`, если работа должна пережить разговор. Выбор
между этими двумя — его, не твой: заводить запись сам этот скилл не вправе;
- **прекратить работу** — человек не готов расширять задачу сейчас. Исход тот
же, запись остаётся как была, вопрос записывается там, где проект держит
вопросы.
**Третьего решения — «доделать как обслуживание» — нет.** Оно и есть то самое
молчаливое изменение поведения, против которого стоит весь разрез: под коммитом,
заявляющим «поменяли оснастку», уехала бы правка, не прошедшая ни чекпоинта, ни
ревью цикла задачи, и не оставившая следа в спеках.
**Сделанное не выбрасывается ни при каком из двух решений.** Оно остаётся в
рабочем дереве незакоммиченным: при переформулировке уезжает в change следующим
прогоном, при отказе — человек решает сам, откатить или оставить. Коммитить его
сообщением про обслуживание нельзя.
**Прогон, дошедший до этого стопа, стоит дороже обычного** — и это довод за
проверку признака на шаге 1, а не после написанного кода.
## OpenSpec здесь не предпосылка
Как и разведке, обслуживанию OpenSpec не нужен: оно не заводит change, не пишет
дельта-спек и не архивирует. Ни один из проходов его плана ревью на дельта-спеки
не завязан — это сказано и в `av-dev:code-review`, раздел «Прогон без change».
Каталога в проекте нет — обслуживание идёт целиком, и деградацией это не
является.
## Планового стопа у этого сценария нет
**И это следствие, а не упрощение.** Чекпоинт решения объясняет человеку
**выбор**: в чём проблема, как решаем, чем рискуем. У обслуживания выбора нет по
построению — что делать, сказано в записи, а критерии приёмки у него самые
дешёвые из всех типов: команда, которая раньше падала или требовала трёх шагов.
Объяснение свелось бы к пересказу задачи её же автору. Стоп, на котором нечего
решать, вырождается в обряд одобрения и обесценивает те стопы, где решать есть
что.
**Мест, где ответа всё же ждут, два, и через оба проходят не все прогоны.**
Первое — стоп по найденной дельте (раздел «Дельта нашлась по ходу»), и плановым
он не является: через него идут те прогоны, где задача оказалась не тем, чем
объявлена. Второе — **реплика о новом на шаге 5**, и она случается, только если
обслуживание завело в документах что-то, чего не было: запрет или инвариант.
Обычный прогон обслуживания не проходит ни через одно из двух.
**Правило необратимого при этом действует полностью** (SKILL.md, «Когда
спрашивать вне чекпоинта»), и здесь оно опаснее, чем кажется. Единственный
сценарий без планового стопа — ровно тот, чья работа чаще прочих лезет в
выкладку, в токены, в хуки и в чужие данные. Правка оснастки выглядит безобидной
до момента, когда её уже не откатить.
## Ход работы
```mermaid
flowchart TD
in["сценарий выбран: обслуживание"]
s1["1. прочитать задачу<br/>критерии приёмки и границы"]
s2["2. правка агентом<br/>гейт тронут — состав снять до правки"]
s3["3. гейт проекта до зелёного"]
s4["4. ревью фиксированным планом<br/>av-dev:code-review, без change"]
s5["5. синк документации — av-dev:doc-sync"]
s6["6. коммит работы — av-dev-git:commit"]
s7["7. закрыть задачу — av-dev:task-track,<br/>вторым коммитом учёта"]
out["исход назван"]
in --> s1 --> s2 --> s3 --> s4 --> s5 --> s6 --> s7 --> out
s1 -.->|"форма правки неизвестна"| stop1["стоп: нужна разведка"]
s2 -.->|"нашлась дельта-спека"| stop2["стоп: назвать тип,<br/>объяснить, дать два решения"]
```
Схема — **сводка**: содержание каждого шага в его разделе ниже, и при расхождении
прав текст.
## Наблюдаемые исходы сценария
Четыре, и каждый обязан быть назван в докладе прямо:
- **сделана** — определение сделанного выполнено целиком;
- **не доведена** — с причиной и с записанным вопросом; названо, что именно
сделано и до какой границы;
- **меняется спека** — работа оказалась шире своего типа. Стоп с объяснением и
двумя решениями человека: переформулировать запись в `fix` или `feature` и
решать её процессом того типа следующим прогоном — либо прекратить. Сделанное
остаётся в рабочем дереве незакоммиченным. Доклад называет **оба**: какой тип
предложен и что человек выбрал;
- **нужна разведка** — форма правки неизвестна (мажорное обновление, смена
сборщика, переезд гейта на другой инструмент) **или сработал триггер ADR**.
Перечень триггеров не пересказывается: он живёт в
[canon.md](../../canon/references/canon.md#adr), и здесь он работает
стоп-признаком — то есть от его точности зависит выбор сценария, а пересказ
расходится с домом молча. Стоп с названной причиной, разведка идёт следующим
прогоном.
**Последний исход — не редкость, и его стоит ждать.** Незнакомое обслуживание —
это выбор подхода с ценой и с тем, что становится невозможным, — предмет чекпоинта
вариантов, а не работы без стопа. И там же решение получает законный источник для
ADR: список источников канон закрыл двумя — архивный `design.md` и записка
разведки, — а обслуживание не производит ни того ни другого.
## Определение сделанного
Задача сделана, когда верно всё:
1. гейт проекта зелёный, и **если правка трогала сам гейт — сверен его состав**,
а не только цвет;
2. ревью проведено фиксированным планом сценария, исход назван по каждой теме
плана, а темы, которых в плане нет, названы в границах покрытия;
3. **документация синхронизирована с принуждённым отрицанием** — каждый документ
канона получил строку;
4. коммит сделан в текущую ветку;
5. **критерии приёмки, если проект их дал, выписаны поимённо, и по каждому назван
оракул и наблюдаемый исход.** Это доклад приёмщику, а не отметка «принято»:
исполнитель и приёмщик здесь совпали, и правило то же, что в решении.
## Шаги
### 1. Прочитать задачу
Прочитай запись. У `chore` обязательны два раздела, и оба нужны тебе прямо
сейчас: **«Затрагивает»** — границы, которые у обслуживания часто не в коде
(конфиг и его образцы, версия зависимости, команда сборки, файл CI), и
**«Критерии приёмки»** — с оракулами.
**Постановка пришла текстом — этих двух разделов нет, и оба нужны тебе тем же
составом.** Границы назови сам и покажи в первой реплике, вместе с типом:
обслуживание чаще прочих сценариев расползается, а границы у него лежат не в
коде, и невидимая граница расползание не удержит. Критериев приёмки в тексте
может не быть вовсе — тогда скажи строкой, что их нет и приёмка идёт по докладу.
Сочинить их себе здесь нельзя даже так, как это делает решение: чекпоинта, на
котором человек их утвердит, у обслуживания нет.
Здесь же обе проверки признака: тип предлагает, отсутствие дельт подтверждает
(раздел «Признак — связка»); постановка текстом типа не объявляла — тогда
называешь его ты, и вслух (раздел «Постановка текстом»). И здесь же — проверка
на незнакомое: если форма правки не известна до начала, а нащупывается по ходу,
объявляй исход **нужна разведка** и не начинай.
**Проверка на «заодно».** Обслуживание любит склеиваться в пачку — обновить
зависимости, переписать сборку и убрать мёртвый код одной задачей. Не мерджится
порознь — это несколько задач: объявляй исход **не доведена** с причиной «задача
не одна» и останавливайся. Нарезкой владеет `av-dev:task-track`, а не ты, и
делать её по ходу нельзя — получится один коммит, в котором обновление
зависимости не отделить от чистки.
### 2. Сделать правку
**Правку делает агент** (SKILL.md, «Кто пишет: письмо уходит агентам»): задание
несёт постановку, конвенции проекта, границы правки и требование довести гейт до
зелёного; возврат — адреса тронутого и исход гейта. Код и конфиги — по конвенциям
проекта. Правка по размеру задачи: чинится названное в записи, соседнее не
улучшается заодно.
**Гейта ещё нет — сказать это, а не изображать сверку.** Первые шаги плана
стройки заводят гейт, сборку и хуки: у них нет ни «до», ни «прежнего», и
определение сделанного через зелёный гейт на них не выполнимо буквально. Такая
задача сделана, когда **заведённое работает на чистом клоне** и это показано в
докладе; пункты 1 и 3 определения ниже закрываются строкой «заводится впервые,
сверять не с чем». Изображать сверку с несуществующим прежним состоянием нельзя —
это ровно то враньё, против которого весь абзац ниже и написан.
**Гейт правится — сверь состав, а не цвет.** Красный, ставший зелёным, виден
сразу; убыли проверок гейт не покажет — он зелёный и до, и после. Это самая
дорогая из возможных правок оснастки: молча выключено то, чем проверяется всё
остальное. Состав проверок и способ его снять — **дело проекта**: он объявляет
их семантикой гейта в `CLAUDE.md`. Снимай исходное состояние **до** правки, по
тому, как проект это описал.
**Исходный состав снимаешь ты, а не агент, и это не мелочь.** Сверка «до и
после» уезжает в твой доклад, а снятое тем же, кто правил, сверкой не является:
агент вернёт состав, который получился, и назовёт его исходным. Снимок делается
до того, как задание ушло.
**Проект состав не описал — скажи строкой доклада, что сверен только цвет.**
Обходного пути не выдумывай: угаданный состав хуже отсутствующего, потому что
читается как сверенный. Это же строка и повод — предложить проекту дописать слот
в `CLAUDE.md`.
### 3. Гейт до зелёного
Прогони гейт и добейся зелёного — он же условие следующего шага: пока гейт
красный, проходы с мнением не запускаются.
**Сразу после зелёного сними отпечаток дерева** (`av-dev:code-review`, ступень 1)
и сохрани его вместе со сводкой и путём к логам шагов. Шаг 4 передаёт их ревью, и
тогда ступень автотестов не гоняет тот же гейт второй раз.
**Поведенческая верификация здесь другая, чем в решении.** Проверяется не новое
поведение, а то, что прежнее не поехало: команда из `CLAUDE.md` поднимается, шаг
сборки отрабатывает, хук ставится на чистом клоне. **У оснастки, заводимой
впервые, прежнего нет** — тогда проверяется, что заведённое работает на чистом
клоне, и в докладе это называется своим именем, а не «регрессий не найдено». Правка, которую нельзя
проверить ничем, кроме «у меня локально работает», называется в докладе строкой.
### 4. Ревью — план фиксирован сценарием
Вызови Skill **`av-dev:code-review`**, дав базу диффа, режим, **план сценария** и
**исход гейта с шага 3** — сводку, путь к логам шагов и отпечаток дерева. Change
ты не передаёшь — его нет.
**План у сценария свой, и он не совпадает с перечнем тем цикла задачи.** Тема
`requirements` там есть, а здесь её предмета нет вовсе; `operations` в цикле
закрыта сверкой с инвариантами внутри `review-code`, а здесь её берёт `basics`
правка оснастки задевает выкладку, откат и соседей чаще, чем что-либо ещё, и
инвариантов на этот счёт у проекта обычно нет.
<!-- дом: план-обслуживания -->
| Тема | Дом | Кто закрывает | Глубина и вход | Когда |
| --- | --- | --- | --- | --- |
| `autotests` | `CLAUDE.md`, семантика гейта | `review-autotests` | как обычно: гейт запускается целиком | всегда |
| `operations` | `architecture.*`, раздел эксплуатации | `review-basics` | **сверка**: дом темы против диффа, потолок 2 | всегда |
| `conventions` + технический разбор | `conventions.*` | `review-code` | как в цикле: дом конвенций целиком, потолок 4 конвенционных, у техники потолка нет; сверка с инвариантами `CLAUDE.md` по темам `security`, `operations`, `architecture`, потолок 1 | дифф трогает код, а не только оснастку |
<!-- /дом: план-обслуживания -->
**Глубина названа в плане потому, что иначе её неоткуда взять.** У `review-basics`
и тема, и глубина приходят заданием — в цикле он держит только свои темы проекта,
а здесь ему дают чужую; без строки плана он взял бы её наугад, то есть по-разному
от прогона к прогону и молча.
**`review-code` идёт тем же составом, что в цикле, и это не совпадение.** Обе его
половины и сверка с инвариантами постоянны — от прогона они не зависят, потому и
переносятся сюда без оговорок. Единственное, что план решает за него, — идти ли
вообще: правка, тронувшая только оснастку, кода не меняла.
Триаж обязателен, как и на всяком прогоне: он единственный сток и единственный,
кто сверяет план с исходом. На его вход подаётся этот план — вместо перечня тем
цикла задачи.
**Условие третьей строки проверяемое, и смотрится оно по диффу**, а не по
намерению: обновление зависимости или правка файла CI кода не трогают, чистка и
перенос — трогают. `review-code` — единственный проход, который вообще говорит
«здесь ошибка в логике», и чистка, прошедшая без него, проверена только на то,
что она собирается.
**Сигнал «просит глубокого ревью» работает и здесь**, но читается иначе: у
обслуживания поднимать нечего — состав фиксирован сценарием. Показалось, что
глубины мало, потому что задача крупнее заявленного, — ищи дельту, а не глубину;
всё прочее уходит строкой «отложено в `av-dev:code-deep-review`».
**Границы покрытия называются полностью:**
- `requirements` — предмета нет, дельта-спек не существует;
- `security` — своего прохода нет; сверена против записанных инвариантов внутри
`review-code`, а он шёл не всегда. Не шёл — тему не смотрел никто, и это
говорится прямо;
- `architecture` — то же: только против инвариантов, и только если шёл `code`.
Отчёт, из которого исчезло «что не смотрел никто», сообщает «проверено», не
сообщая, что именно.
Отработка — как в решении: помеченное `инлайн` чинит **агент** (SKILL.md, «Кто
пишет»), находки уходят ему дословно с оракулом, гейт после правок гоняет он же,
логировать их не надо; `развилка` — вопросом в запись, и агенту она не отдаётся.
Отложенные находки собери в секцию доклада `Урожай`.
**Задачи из урожая — по слову человека, и спрашивается это репликой шага 5**,
вместе с новым в документах: правило общее для всех прогонов конвейера
(`av-dev:code-review`, «Что происходит с находками дальше»), и обслуживание не
исключение. Сказал «заводим» — зовёшь `av-dev:task-track` сам, сценарий «задачи
из ревью и аудита»; не сказал — урожай остаётся строками доклада.
### 5. Синк документации — главный шаг этого сценария, и делает его агент
**Синк уходит агенту** (SKILL.md, «Кто пишет»): работа письменная и нормирована
чек-листом скилла `av-dev:doc-sync`, а не суждением оркестратора. В задании —
корень проекта, база диффа, что было тронуто правкой, требование принуждённого
отрицания и требование довести гейт до зелёного после правок. Вычитку языка
`av-dev:doc-sync` зовёт сам.
**Правило то же и такое же жёсткое: принуждённое отрицание** — каждый документ
канона либо назван обновлённым, либо получает «не требуется, потому что…».
Нетронутые группируются одной строкой. Возврат приходит в этой же форме и уезжает
в доклад целиком: тронутое без нетронутого не отличается от невыполненного шага.
**Второе правило синка тоже действует: отражение пишется молча, новое
предлагается** (дом — раздел «Два рода правок» скилла `av-dev:doc-sync`). Здесь
оно почти ничего не стоит: обслуживание двигает **факты** — команды, шаги гейта,
зависимости поимённо, пути, имя ветки, числа настроек, — а факт в документе,
разошедшийся с кодом, это отражение по определению.
**Повод для реплики у этого сценария один — новый запрет или инвариант в
`CLAUDE.md`**: он свяжет все будущие задачи, и заводить его молча нельзя.
Сужение проверок в `review.*` поводом не является, хотя тоже новое: проверки
сузил человек, и слово по ним уже сказано (`av-dev:doc-sync`, «Два рода правок»).
К этому же поводу примыкает урожай ревью с шага 4 — спрашиваются они одной
репликой, а не двумя.
**Нового нет — реплики нет**, и шаг кончается возвратом агента; так идёт
большинство прогонов обслуживания.
**Реплика была — идёт второй заход тем же агентом**, и на нём висит то же, что в
решении: письмо одобренного, вычитка `doc-wording` по всей пачке и гейт до
зелёного. Первый заход снимает их с себя ровно тогда, когда вернул непустой
список предложений, — порядок и его довод описаны в [solve.md](solve.md), шаг 6.
Задачи из урожая при этом заводишь **ты сам** вызовом `av-dev:task-track`, а не
агент: индексы учёта правит тот, кто коммитит.
**Здесь этот шаг весит больше, чем в решении, и вот почему.** Обслуживание не
меняет поведения — значит, почти всё, что оно меняет, это документация: команды,
шаги гейта, зависимости поимённо, пути, имя основной ветки, настройки с числовым
значением, место механизации правила. Ровно эти факты `doc-code-drift` и сверяет
с кодом (перечень закрыт, живёт в каноне) — то есть сценарий, чаще всех прочих
двигающий сверяемые факты, обязан отчитаться по ним раньше всех прочих.
Отдельно один документ, которого нет в перечне тем, а синку он нужен:
**`conventions.*`, раздел «Механизировано»** — если правило переехало в линтер, и
тогда его проза из конвенций **удаляется**, а не остаётся вторым домом.
**`adr/` этот сценарий не пополняет, и это не пропуск.** У ADR закрытый список
источников — архивный `design.md` либо записка разведки, — и ни того ни другого
обслуживание не производит. Поэтому триггеры ADR здесь работают **стоп-признаком,
а не поводом завести запись**: сработал любой из них — сценарий выбран неверно,
объявляй исход **нужна разведка** и останавливайся. Перечень триггеров — в
[canon.md](../../canon/references/canon.md#adr) и здесь не пересказывается. Решение с ценой обязано
пройти чекпоинт вариантов, а не появиться в коммите обслуживания, чей смысл —
«ничего не решали, поменяли оснастку».
Список документов и их триггеров здесь не дублируется — он в чек-листе скилла
`av-dev:doc-sync`; копия уже однажды разошлась с оригиналом. Документов канона в
проекте нет — синка нет вовсе: скажи это исходом и предложи завести канон
скиллом `av-dev:canon`.
### 6. Коммит
Коммить **в текущую ветку** (`git rev-parse --abbrev-ref HEAD`), сам ветку не
создавай и не переключай, ничего не пушь.
Сообщение — по-русски, **скиллом `av-dev-git:commit`**. Вызов не разрешился —
напиши сам и скажи строкой, что форму коммита не сверял никто. Одна задача — один
осмысленный коммит.
### 7. Закрыть задачу — после коммита, не раньше
**Вызови Skill `av-dev:task-track`** и попроси закрыть задачу как реализованную.
Порядок обязателен: закрытие удаляет файл задачи, и сделанное до коммита оно
оставило бы задачу закрытой без следа работы, если шаг 6 упадёт.
**Закрытие тоже коммитится — вторым коммитом, тут же**, сообщением про учёт:
`закрыта задача <slug>`. Каталога задач в проекте нет — ничего не выдумывай:
скажи, что учёт остаётся за владельцем, и назови исход.
**Постановка пришла текстом — шага нет вовсе**: записи не было, закрывать нечего,
след работы — коммит шага 6. Заводить запись задним числом ради закрытия нельзя.
## Границы: чего обслуживание не делает
- **Не меняет поведения.** Обнаружилось, что меняет, — стоп с исходом «меняется
спека»: назвать тип, объяснить, дать два решения. Это единственная граница
сценария, у которой есть проверяемый признак, и она же единственная, которую
выгодно нарушить молча.
- **Не переписывает запись задачи сам.** Тип ты **предлагаешь** с причиной,
меняет его `av-dev:task-track` и только после ответа человека: исполнитель,
переклеивший тип на ходу, назначает себе другой процесс и другую глубину
проверки.
- **Не решает, нужна ли работа.** Как и оба соседних сценария: «надо ли» —
вопрос человека.
- **Не нарезает пачку на задачи.** «Обновить зависимости и переписать сборку» —
это `av-dev:task-track` и его правила нарезки.
- **Не выбирает форму правки, когда она незнакома, и не принимает решений с
ценой.** Мажорное обновление, смена инструмента, намеренный отказ идут
разведкой: там есть чекпоинт вариантов и законный источник для ADR, здесь нет
ни того ни другого.
- **Не заводит задачи из урожая ревью.** Урожай передаётся списком.
## Доклад обслуживания
Общее ядро доклада — в SKILL.md; сверх него сценарий обязан назвать:
- **что подтвердило признак** — тип записи и то, что дельта-спек не нашлось;
постановка пришла текстом — **тип назвал ты**, и это говорится прямо, вместе с
границами, которые ты объявил себе сам;
дельта нашлась — **какой тип предложен, с причиной, и что человек выбрал**:
переформулировать или прекратить;
- **что стало иначе для разработчика** — одной фразой, адресуясь ему, а не
выдуманному пользователю;
- **состав гейта до и после**, если правка его трогала; не сверялся — почему;
- по каждому критерию приёмки: **оракул и наблюдаемый исход**;
- **`Урожай`** — отложенные находки списком и **что человек по нему решил**;
- **что заведено нового в документах** и что предложено и отвергнуто — по именам
записей; отказ виден только здесь;
- **сигнал сверки** — строка синка о том, сколько задач сделано с прошлого
прогона `av-dev:doc-healthcheck`, либо что сверки не было ни разу;
- **строка границ покрытия**: план сценария фиксирован; темы `requirements` в нём
нет — её не смотрел никто, а `security` и `architecture` смотрелись только
против записанных инвариантов, и то если шёл проход `code`.
## Тонкости сценария
- **Самый частый способ соврать этим сценарием — назвать `chore` то, что меняет
поведение.** Тип, оставшийся от первой формулировки, врёт ровно там, где по
нему выбирают путь; проверка признака стоит одного чтения записи и делается на
шаге 1, а не после написанного кода.
- **Отсутствие чекпоинта не делает сценарий автономнее прочих.** Правило
необратимого здесь то же, и срабатывает оно чаще: выкладка, токены, хуки,
чужие данные — обычное содержимое задач обслуживания.
- **Зелёный гейт после правки гейта ничего не доказывает.** Это единственное
место конвейера, где инструмент проверяет сам себя, и потому состав сверяется
отдельно от цвета.
- **Правка оснастки, сделанная «заодно» внутри чужой задачи, этим сценарием не
проходит вовсе** — она едет в чужом коммите и не получает ни своего ревью, ни
своей строки синка. Заметил нужную правку по ходу решения — вопрос в запись,
а не правка мимоходом.
@@ -0,0 +1,426 @@
# Сценарий «разведка»
Отвечает на вопрос задачи и записывает ответ туда, где он переживёт переписку.
Исход — **знание**: уточнённые документы и уточнённые задачи. **Кода этот
сценарий не пишет и change не заводит.**
Сценарий выбирается развилкой на входе скилла ([SKILL.md](../SKILL.md), раздел
«Развилка: какой сценарий») и называется вслух первой репликой. Здесь только его
ход; общее для всех трёх сценариев — вход, чего может не быть, правило
необратимого, доклад — живёт в SKILL.md и тут не пересказывается.
**Разведка кончается своим исходом, а не переходом к коду.** Выбранный способ
реализует сценарий решения, и запускает его **человек**, следующим прогоном по
уточнённой записи. Причина не в церемонии: разведка только что переписала
постановку, и брать её в работу тем же заходом значит решать за человека, стоит
ли делать это сейчас, — а это приоритет, и он не наш.
## OpenSpec здесь не предпосылка
**OpenSpec этому сценарию не нужен**, и это единственное место скилла, где он не
предпосылка. Разведка не заводит change и не пишет дельта-спеки: её артефакты —
документы канона и записи каталога задач. Скилл `opsx:explore` полезен и зовётся,
когда внешний плагин установлен; не разрешился — разведка идёт чтением документов,
кода и внешних источников, и это говорится строкой доклада, а не отменяет
работу.
## Кого зовёт этот сценарий
`av-dev:doc-sync` (ответ уезжает в документы канона), `av-dev:task-track`
(задачи заводятся и уточняются), `av-dev-git:commit`. Правило обращения к соседям
и правило «чего может не быть» — общие, они в [SKILL.md](../SKILL.md).
**Агентов-исполнителей у разведки нет** (SKILL.md, «Кто пишет: письмо уходит
агентам»), и это не пропуск. Её письмо — записка в документы канона и записи
задач, то есть тот самый текст, из которого собираются чекпоинт вариантов и
доклад: отданный агенту, он вернулся бы пересказом. Вычитку разведка всё же
отдаёт — `doc-wording`, `task-form`, `task-wording`: там судят написанное, а не
пишут.
**Отсутствие канона бьёт по разведке сильнее, чем по решению**, и сказать об этом
строкой мало: без документов у ответа нет дома, и знание осядет в переписке.
Назови исход и предложи `av-dev:canon`; работу не останавливай, но адрес
ответа тогда выбираешь сам и говоришь об этом вслух.
## Что этот сценарий требует от входа
Вход общий у всех трёх сценариев (SKILL.md, раздел «Вход»); своего здесь три
условия.
**Паспорт читается раньше кода.** Граница домена и «чем проект **не** является»
отсекают половину вариантов до того, как их начнут сравнивать, — а сравнение
вариантов и есть работа этого сценария.
**Сырьё — `research` без раздела «Вопрос» — не берётся.** Исход «не доведена» с
причиной «запись это сырьё»: у неё нет вопроса, а разведка без вопроса
превращается в чтение всего подряд с отчётом «интересно, но неприменимо».
Дописывать вопрос за автора не твоя работа — этим занят штурм сырья в
`av-dev:task-track`. Назови, чего не хватает, и остановись.
**Разведка пришла текстом — сформулируй вопрос сам одной фразой и покажи
формулировку в первой же реплике.** Разведка, чей вопрос не назван вслух,
признаётся удавшейся любым результатом. Это частный случай общего правила
(SKILL.md, «Постановка текстом»): у разведки показать надо не только предмет
работы, но и сам вопрос, потому что предмет разведки — он и есть. Вместе с
вопросом называются рамки и адрес ответа (шаг 1).
## Ход работы
```mermaid
flowchart TD
in["вход: файл, слаг или текст"]
s1["1. вопрос и рамки<br/>сырьё без «Вопроса» — отказ"]
s2["2. разведка: документы, код,<br/>внешние источники, opsx:explore"]
s3(["3. ЧЕКПОИНТ: варианты<br/>2–4 способа, цена каждого,<br/>что становится невозможным"])
s4["4. ответ в документы канона<br/>av-dev:doc-sync"]
s5["5. задачи: завести и уточнить<br/>av-dev:task-track"]
s6["6. вычитка написанного:<br/>документы и записи задач"]
s7["7. гейт проекта, затем коммит<br/>av-dev-git:commit"]
s8["8. закрыть разведку — av-dev:task-track,<br/>вторым коммитом учёта"]
out["исход назван: знание, задачи,<br/>отказ или «не доведена»"]
in --> s1 --> s2 --> s3
s3 -->|"выбран способ,<br/>отказ или знание"| s4
s3 -.->|"вопрос не тот"| s1
s4 --> s5 --> s6 --> s7 --> s8 --> out
```
Схема — **сводка**: содержание каждого шага в его разделе ниже, и при расхождении
прав текст.
## Плановый стоп сценария
**До чекпоинта умолчание прежнее — делать, а не спрашивать.** Развилка, найденная
по ходу разведки, не задаётся отдельным вопросом: она копится в чекпоинт, который
рядом и стоит дёшево.
Стоп здесь один, и стоит он **перед записью**. Причина в цене: пока варианты
живут в контексте, смена решения стоит абзаца; записанный в документы и разложенный
на задачи выбор стоит правки документов и разбора беклога. Второго стопа — «покажи,
что именно уедет в документы» — нет намеренно: всё, что пишет разведка, лежит в
git и читается диффом, а второй стоп на каждой разведке вырождается в ритуал
одобрения.
**Правило необратимого действует и здесь** (SKILL.md, «Когда спрашивать вне
чекпоинта»), и разведка обманчива: она кажется безобидной, а замер лезет туда, где
живут данные — прогон на боевой базе, запрос к внешнему платному источнику. Здесь
ошибка не откатывается правкой текста.
## Границы: чего разведка не делает
- **Кодом.** Ни строки, включая «маленький черновик, чтобы проверить». Замер,
требующий кода, — это отдельная задача, и её нужно назвать, а не написать по
ходу. Исключение ровно одно и оно не про изменение системы: одноразовый
**читающий** прогон (запрос, замер, скрипт в песочнице), чей результат уезжает
в ответ с происхождением и который ничего не оставляет в репозитории.
- **Местом в списке.** Заведённая задача встаёт в конец своей секции; куда её
поставить, решает человек — на доработке грумингом (`av-dev:task-groom`), на
стройке сразу же, по зависимости. Разведка, сама ставящая свой исход первым,
назначает место тому, что только что придумала.
- **Форматом задач и документов.** Индексы и документы руками не правятся: их
ведут `av-dev:task-track` и `av-dev:doc-sync`. Твоё — содержание ответа, их —
форма и дом.
- **Определением ценности.** «Нужно ли это делать вообще» — вопрос человека.
Разведка отвечает «как это можно сделать и чего каждый способ стоит».
- **Решением задачи.** Выбранный способ реализует сценарий решения, и запускает
его человек следующим прогоном. Перейти в него по ходу нельзя — это тот самый
переход, ради невозможности которого сценарии и разведены.
## Наблюдаемые исходы сценария
Четыре, и каждый обязан быть назван в докладе прямо. Первые три — это те самые
«заведены задачи, записано знание, отказ», которыми кончается разведка по
определению типа `research`:
- **способ выбран** — ответ записан, задачи заведены или уточнены, они готовы к
взятию. Дальше сценарий решения, следующим прогоном, и запускает его человек;
- **знание записано** — вопрос закрыт, задач он не породил: ответ ценен сам по
себе (замер, устройство внешнего формата, «так работает и менять не нужно»);
- **отказ** — проверили, проблемы нет либо подход отвергнут. **Полноправный
исход, а не пустая работа**: «не делаем и вот почему» экономит всю работу,
которая иначе была бы сделана. Причина записывается — без неё через квартал
разведку закажут заново;
- **не доведена** — вопроса нет (сырьё), рамки исчерпаны без ответа, или человек
на чекпоинте не одобрил ни одного варианта. Названо, что успели узнать и до
какой границы.
## Определение сделанного для разведки
У сценария решения оно своё (SKILL.md); здесь — короткое и другое. Разведка
сделана, когда верно всё:
1. **ответ записан по адресу, который назвала задача** — раздел «Куда ляжет
ответ». Адреса не было, а вопрос был — ты назначил адрес сам и сказал об этом
строкой;
2. **у каждого числа названо происхождение** — команда или условия, которыми оно получено.
Число без источника проход ревью обязан читать как условие, а не как замер, и
разведка, оставившая голые числа, вредна: по ним будут решать;
3. **отвергнутые варианты названы с причиной**. Отвергнутое без причины
возвращается на следующей разведке как новая идея;
4. задачи, которые исход породил, заведены — или явно сказано, что не породил;
5. **написанное вычитано** — документы агентом `doc-wording`, записи задач
проходами `task-form` и `task-wording`, каждый по своей пачке;
6. написанное закоммичено, разведка закрыта.
## Шаги
### 1. Вопрос и рамки
Прочитай запись. У типа `research` два обязательных раздела, и оба нужны тебе
прямо сейчас: **«Вопрос»** — на что отвечаем, **«Куда ляжет ответ»** — по какому
адресу он ляжет. Адрес назван заранее не из аккуратности: ответ, не имеющий дома,
остаётся в переписке, и через квартал разведку заказывают заново.
**Адрес назначает автор записи, а не ты.** Запись из каталога без него до тебя
не доходит: `ready` требует непустыми оба раздела и откажет — это стоп со
строкой, чего не хватает, а не повод дописать за автора. Иначе исполнитель сам
назначает себе приёмку, а приёмка разведки — это и есть записанный по названному
адресу ответ.
**Адрес назначаешь ты ровно в одном случае** — когда записи нет вовсе: разведка
пришла текстом или в проекте нет учёта задач. Тогда скажи об этом строкой, а
выбирай по канону, а не по удобству:
| Что узнали | Дом ответа |
| --- | --- |
| наблюдение о внешнем мире, замер с происхождением | `docs/research/` |
| решение с ценой: намеренный отказ, дорогой откат | `docs/adr/` |
| факт об устройстве системы | тема `architecture` (или своя тема проекта) |
| граница домена, «чем проект **не** является» | `passport` |
| ответ нужен только этой работе | тело самой записи |
Раздел **«Рамки»**, если он есть, — это граница разведки: сколько копаем, какие
источники, что заведомо вне. Рамок нет, а вопрос широкий — **назначь их сам и
покажи в первой реплике**. Разведка без рамок утекает: она всегда может узнать
ещё немного, и признак «достаточно» изнутри не виден.
Здесь же проверка на «не тот вопрос»: если из записи видно, что отвечать надо на
другое, скажи это сразу, а не после разведки.
### 2. Разведка
Порядок чтения — от дешёвого к дорогому, и он не произволен:
1. **документы канона проекта** — половина вопросов уже отвечена там, и разведка,
начатая с кода, переоткрывает написанное;
2. **код и его история**`git log` по узлу отвечает на «почему так» чаще, чем
кажется;
3. **внешние источники** — документация формата, чужой опыт, спецификации;
4. **замер** — если вопрос про числа. Числа снимаются с происхождением, иначе они
бесполезны на следующем шаге.
**Скилл `opsx:explore`** — законный инструмент этого шага, если плагин в проекте
есть: он держит форму размышления и не даёт ему растечься. **В explore не пишем
код.** Вызов не разрешился — работай чтением, скажи это строкой.
Развилку разведки **не записывай вопросом** — она и есть предмет следующего шага.
### 3. Чекпоинт: варианты
**Остановись и покажи человеку способы решить.** Это плановый стоп сценария и
единственное место, где разведка ждёт ответа.
Форма — короткая, экран текста:
- **вопрос**, на который отвечаем, одной фразой (он уже есть в записи);
- **24 варианта**, не больше. Больше четырёх — это не выбор, а список: человек
не сравнит, а признает свою неспособность сравнить и попросит рекомендацию.
У каждого варианта: в чём суть простыми словами, **что даёт**, **чего стоит**,
**что становится невозможным** (это ловится хуже всего и стоит дороже всего);
- **рекомендация с причиной**. Человек чаще соглашается, чем выбирает заново;
- что известно **недостоверно** и как это проверить, если проверять дёшево;
- **что уедет в документы и в задачи**, если возражений нет, — одной строкой.
Это не второй стоп, а предупреждение: человек видит объём последствий там же,
где принимает решение.
Что нельзя: приносить варианты, различающиеся только реализацией; прятать
отвергнутое (отвергнутый вариант с названной причиной — половина будущего ADR);
приносить один вариант и называть это выбором.
Проверка на простой язык — общая у трёх сценариев:
<!-- копия: чекпоинт-простой-язык из av-dev/skills/code-resolve/references/solve.md -->
**в тексте нет `SHALL`, нет имён файлов и функций там, где вещь называется
по-русски, и нет слов, которых нет в паспорте проекта.**
<!-- /копия: чекпоинт-простой-язык -->
Исходы чекпоинта:
- **выбран способ** — идёшь на шаг 4, исход разведки будет «способ выбран». Кода
ты по нему не пишешь: сценарий кончается записью и коммитом;
- **ответ и есть результат** — идёшь на шаг 4, исход «знание записано» или
«отказ»;
- **вопрос не тот** — возвращаешься на шаг 1: переформулируй вопрос и скажи, что
из разведанного остаётся в силе;
- **ни один вариант не одобрен** — исход «не доведена» с причиной. Записывается
всё равно то, что узнано (шаг 4): выброшенная разведка будет заказана заново.
### 4. Ответ в документы канона
**Вызови Skill `av-dev:doc-sync`**: он владеет содержимым документов канона.
Передай ему ответ, адрес из шага 1 и происхождение каждого числа — писать содержание
за тебя он не будет, но дом и форму держит он. Вычитка языка — тоже его агент, но
момент её назван отдельно, шагом 6: пачка собирается из шагов 4 и 5 и до конца
пятого не полна.
**Что именно уезжает:**
- **ответ на вопрос** — по адресу из шага 1;
- **отвергнутые варианты с причинами.** Это половина будущего ADR и единственная
защита от повторной разведки того же самого;
- **решение с ценой — в ADR**, если оно проходит [триггер
канона](../../canon/references/canon.md#adr). У разведки, кончившейся без
кода, `design.md` не будет никогда, поэтому здесь ADR цитирует **записку
разведки**, а не архивный change; канон это допускает прямо, и в записи
источник называется.
**Правило принуждённого отрицания здесь не действует.** Это не синк: разведка
трогает те документы, которых коснулся её ответ, и перебирать весь канон ей
незачем. Но **названо должно быть каждое место, куда ты писал**, — доклад без
перечня адресов неотличим от доклада о ненаписанном.
**Правило «новое по слову» здесь тоже не задаёт второго вопроса**, хотя ответ
разведки — новое от первой до последней строки. Слово уже сказано **чекпоинтом
вариантов**: человек выбрал вариант и тем самым заказал запись. Спросить ещё раз
значило бы переспросить только что одобренное — и заодно предложить выбросить
работу, ради которой прогон и шёл. Что записать нового сверх выбранного —
например ADR по решению с ценой, — предлагается, как везде.
**Документов канона в проекте нет** — писать ответ некуда: назови это исходом,
предложи завести канон скиллом `av-dev:canon` и оставь ответ в докладе
целиком, чтобы работа не пропала. Заводить `docs/` мимо канона нельзя.
### 5. Задачи: завести и уточнить
**Вызови Skill `av-dev:task-track`.** Он владеет форматом, дедупом и индексами;
путь к его скрипту не выясняй и индексы руками не правь.
Что просишь сделать:
- **уточнить саму разведку** — если её вопрос по ходу изменился;
- **уточнить существующие задачи** — разведка часто отвечает не «что делать», а
«что в поставленном неверно»: постановка, границы в разделе «Затрагивает»,
критерии приёмки;
- **завести новые задачи**, если исход их породил. Формулировки приноси готовыми:
заголовок, «зачем», тип, границы. Нарезку на независимо полезные части и
проверку на дубли делает он — у него на это свои правила и свой сценарий.
**Пачку задач показывай списком, прежде чем заводить.** Разведка — самый лёгкий
способ надуть беклог: она порождает идеи быстрее, чем кто-либо успевает их
оценивать, а заведённая задача живёт, пока её кто-то не выкинет руками.
Каталога задач в проекте нет — задачи остаются **списком формулировок в
докладе**, и это говорится строкой: учёт работ остаётся за владельцем.
### 6. Вычитка написанного — до гейта, не после
Разведка правит **две вещи сразу**: документы канона (шаг 4) и записи каталога
задач (шаг 5). Обе — текст, и портится он в момент письма, а машина этого не
видит: `docs.py check` и `tasks.py check` смотрят форму раскладки, а не залог,
оценку без факта, жаргон и термин, которого нет в паспорте проекта.
Пачка **собирается только сейчас**, и поэтому шаг стоит здесь: раньше пятого шага
она не полна, а после коммита вычитка уже правит закоммиченное.
- **Документы** — агент `doc-wording`, владеет им `av-dev:doc-sync` (раздел
«Вычитка»). Пачка — адреса, названные на шаге 4, включая `docs/adr/` и
`docs/research/`.
- **Записи задач** — два прохода, сперва `task-form`, затем `task-wording`;
владеет ими `av-dev:task-track` (раздел «Вычитка: два прохода»). Пачка —
заведённые и уточнённые на шаге 5 записи. Заголовок и «зачем» правятся **не
молча**: покажи предложенное вместе с тем, что было.
**Что не правилось, то не вычитывается.** Разведка, кончившаяся одним документом
и ни одной задачей, зовёт один проход, и это не пропуск — это названная строкой
пачка. Скилл-владелец уже прогнал свою пачку по ходу шага — назови это и второй
раз тот же файл не гоняй.
**Судей канона — `doc-consistency` и `doc-code-drift` — здесь не зови.** Они
идут на весь канон разом, стоят дорого, и владеет ими `av-dev:doc-healthcheck`,
момент вызова которого выбирает человек. Нужно суждение о согласованности — скажи
строкой и предложи `healthcheck`, а не зови агентов сам.
Ни один проход ничего не правит: они возвращают готовые формулировки,
подставляешь их ты — и уже с подставленными идёшь на гейт.
Проходы вычитки — агенты этого же плагина, и разрешаются они всегда. Не
разрешились — это поломка установки, а не раскладки проекта: скажи строкой, что
написанное не вычитывал никто, и обходного пути не выдумывай.
### 7. Гейт и коммит
**Сперва гейт проекта** — тот же, что гоняет сценарий решения, и по той же
причине: разведка только что правила документы канона и индексы задач, а это
ровно то, что машина умеет проверить (`docs.py check`, `tasks.py check --dir`,
битые ссылки). Красный гейт чинится здесь, а не оставляется следующему прогону:
он придёт за код и получит чужую поломку в наследство.
Гейта в проекте нет — скажи строкой, что записанное не проверял никто.
Коммить **в текущую ветку** (`git rev-parse --abbrev-ref HEAD`), сам ветку не
создавай и не переключай, ничего не пушь.
Сообщение — по-русски, **скиллом `av-dev-git:commit`**: форму сообщения держит
он, и здесь она не пересказывается. Вызов не разрешился — напиши сообщение сам и
скажи строкой, что форму коммита не сверял никто.
Одна разведка — один осмысленный коммит: записанный ответ и заведённые задачи
уезжают вместе, потому что порознь они полуправда.
### 8. Закрыть разведку — после коммита, не раньше
**Вызови Skill `av-dev:task-track`** и попроси закрыть запись: ответ записан —
`close --implemented`, ушла без ответа — `close --reason`, и строка уезжает в
кладбище.
**Порядок обязателен.** Закрытие удаляет файл задачи; сделанное до коммита оно
оставило бы разведку закрытой без единого следа работы, если шаг 7 упадёт. У
разведки это опаснее, чем у решения: следом работы там служит код, а здесь —
только записанный ответ. Закрытая разведка без него не оставляет следа вообще —
файл задачи удалён, ответ был в переписке.
**Закрытие тоже коммитится — вторым коммитом, тут же.** Сообщение короткое, про
учёт, а не про работу: `закрыта задача <slug>`. Правило «одна разведка — один
коммит» про работу, а учёт — не работа.
**Разведка пришла текстом — шага нет вовсе**: записи не было, закрывать нечего.
Следом работы здесь служит не код, а **записанный по названному адресу ответ**
он уехал в коммит шагом 7, и потому отсутствие записи разведке ничем не грозит.
Ответ записать было некуда и он остался в докладе — вот это как раз тот случай,
когда от прогона не осталось ничего: скажи об этом прямо, а не одной строкой
среди прочих.
Каталога задач в проекте нет — **ничего не выдумывай**: скажи в докладе, что
учёт задач остаётся за владельцем, и назови исход.
## Доклад разведки
Общая форма доклада — в SKILL.md; у разведки он свой, потому что докладывать
нечего из того, о чём спрашивают решение: ни критериев приёмки, ни архивного
change, ни исхода ревью — кода она не писала. Коротко, и в нём обязательно:
- **исход** одним из четырёх слов;
- **вопрос и ответ** — по фразе на каждое. Ответ, который не сворачивается во
фразу, — признак того, что разведка отвечала не на один вопрос;
- **куда записано** — перечнем адресов, а не «документация обновлена»;
- **какие задачи заведены и уточнены** — слагами;
- **что вычитано и чем** — пачка документов и пачка записей, каждая со своими
проходами; не вычитанное называется прямо, вместе с причиной;
- **сигнал сверки** — строка синка о том, сколько задач сделано с прошлого
прогона `av-dev:doc-healthcheck`, либо что сверки не было ни разу;
- **что осталось неизвестным** и чего это стоит: разведка без этой строки
сообщает «выяснено», не сообщая, что именно осталось не выяснено;
- **рамки**, если они ограничили работу: докуда копали и почему остановились.
## Тонкости
- **Ответ «зависит от» — не ответ.** Если разведка кончилась развилкой, её исход
— варианты с ценой, а не пересказ обеих сторон без рекомендации.
- **Отрицательный результат записывается так же тщательно, как положительный.**
Соблазн не писать его велик: кажется, что писать нечего. Через квартал именно
этой записи и не хватит.
- **Чужую разведку не переоткрывай молча.** Нашёл в `docs/research/` или в
`docs/adr/` ответ на свой вопрос — исход «знание записано» со ссылкой, и это
лучшая из возможных разведок: она стоила одного чтения.
@@ -0,0 +1,529 @@
# Сценарий «решение»
Способ решения известен, спорно только как. Проводит задачу от постановки до
закрытия и **пишет код**: цикл Spec Driven Development с двумя плановыми стопами —
объяснением сразу после предложения и репликой о новом после ревью.
Сценарий выбирается развилкой на входе скилла ([SKILL.md](../SKILL.md), раздел
«Развилка: какой сценарий») и называется вслух первой репликой. Здесь только его
ход; общее для всех трёх сценариев — вход, чего может не быть, правило
записанного вопроса, правило необратимого — живёт в SKILL.md и тут не
пересказывается.
**OpenSpec — жёсткая предпосылка именно этого сценария** (SKILL.md,
«Предпосылки»): на нём стоят шаги 2, 4 и 6 и проход `review-specs`.
Тонкая обёртка над каноническими скиллами `opsx:propose` / `opsx:apply` /
`opsx:archive` — их шаги не переизобретаются, а **зовёт их агент**, не ты
(SKILL.md, «Кто пишет: письмо уходит агентам»). Ревью — скилл
`av-dev:code-review`; состав его прогона постоянный, выбирать и размечать нечего.
## Ход работы
```mermaid
flowchart TD
in["сценарий выбран: решение"]
s1["1. прочитать задачу<br/>критерии приёмки выписать сразу"]
s2["2. opsx:propose — change, дельта-спеки,<br/>tasks.md — агентом"]
s3(["3. ЧЕКПОИНТ: объяснение<br/>в чём проблема, как решаем,<br/>чем рискуем"])
s4["4. opsx:apply — код, гейт,<br/>поведенческая верификация — агентом"]
s5["5. ревью кода — постоянный состав<br/>+ отработка замечаний агентом"]
s6["6. opsx:archive + отражение в документах —<br/>одним агентом"]
s6q(["РЕПЛИКА: что заводим из нового —<br/>ADR, конвенция, задачи из урожая"])
s6b["такт 3: задачи — оркестратором,<br/>документы, вычитка и гейт — агентом"]
s7["7. коммит работы — av-dev-git:commit"]
s8["8. закрыть задачу — av-dev:task-track,<br/>вторым коммитом учёта"]
in --> s1
s1 --> s2 --> s3 --> s4 --> s5 --> s6 --> s6q --> s6b --> s7 --> s8
s3 -.->|"скорректировать:<br/>правка спек и дизайна"| s3
s5 -.->|"находка отменяет дизайн:<br/>меняются дельта-спеки"| s3
s6 -.->|"нового нет:<br/>реплики нет"| s7
```
Схема — **сводка**: содержание каждого шага в его разделе ниже, и при
расхождении прав текст.
## Наблюдаемые исходы сценария
Четыре, и каждый обязан быть назван в докладе прямо:
- **сделана** — определение сделанного выполнено целиком;
- **не доведена** — с причиной и с записанным вопросом; что именно сделано и до
какой границы, названо явно. Сюда же попадает чекпоинт, на котором человек
решение не одобрил;
- **оказалась крупнее задачи** — распознаётся **до заведения change**, иначе его
придётся выбрасывать. Дальше — декомпозиция, и это не работа этого скилла;
- **нужна разведка** — очевидного способа решения нет, и это выяснилось уже в
работе. Стоп с названной причиной; кода не написано ни строки **намеренно**.
Разведка идёт следующим прогоном.
## Определение сделанного
Задача сделана, когда верно всё:
1. гейт проекта зелёный;
2. ревью проведено, **перечень тем сверен с исходом по каждой**, темы без отчёта
и без дома названы в границах покрытия;
3. решение прошло чекпоинт и не разошлось с одобренным — либо разошлось, и
чекпоинт был пройден заново;
4. change заархивирован, дельты влиты в актуальные спеки, и по **каждому**
документу канона назван исход — правка, предложение или отрицание с причиной;
5. коммит сделан в текущую ветку;
6. **критерии приёмки, если проект их дал, выписаны поимённо, и по каждому назван
оракул и наблюдаемый исход** — «прогнал вот это, увидел вот то». Это **доклад,
а не сертификация: приёмка — не работа этого скилла.** Исполнитель, ставящий
себе галочку «принято», проверяет свою работу своим же взглядом — по границе
это может делать только приёмщик, разведённый с исполнителем. Критерии приходят
снаружи; скилл их не сочиняет и не занижает. Расхождение «по каждому критерию
исход есть, а суть задачи не достигнута» — дефект критериев, и о нём
сообщается, а не молча дорабатывается.
## Шаги
### 1. Прочитать задачу
Прочитай запись и связанные спеки и черновики.
Проект даёт задаче **критерии приёмки** — выпиши их сразу: на шаге 2 они уезжают
в `tasks.md` change. Файл задачи может быть удалён до коммита, а критерии обязаны
его пережить.
**Постановка пришла текстом** (SKILL.md, «Постановка текстом») — записи нет,
читаешь сам текст. Критерии в нём бывают редко: выпиши то, что там есть, а
недостающие **предложи на чекпоинте шага 3** и считай их данными только после
ответа человека. Сам себе критерии не проставляешь — правило то же, что и с
записью: они приходят снаружи, и подсунуть их себе значит назначить себе приёмку.
Человек критериев не назвал — скажи строкой, что задача идёт без них и приёмка
пойдёт по объяснению чекпоинта.
Здесь же проверка на «крупнее задачи»: видно, что одним заходом это не
мерджится, — объявляй исход **до** заведения change.
### 2. Завести change — `opsx:propose`
**Скилл `opsx:propose` зовёт агент** (SKILL.md, «Кто пишет»). В задании:
постановка — файл задачи либо её текст дословно, — критерии приёмки, если они
были, и требование прогнать `openspec validate --strict <id>`. Возврат:
идентификатор change, дельты адресами и исход валидации.
Шаг оставляет `proposal.md`, дизайн, дельта-спеки
(`ADDED`/`MODIFIED`/`REMOVED Requirements`) и `tasks.md`. Форму держит сам
`opsx:propose`, и требования к ней идут агенту заданием: каждое
`### Requirement` содержит `SHALL`/`MUST`, структурные заголовки английские,
сценарии — `GIVEN/WHEN/THEN`.
**`proposal.md` и `design.md` после возврата читаешь сам** — из них собирается
чекпоинт шага 3, и держать их в контексте это твоя работа, а не переполнение.
Кода нет, читать нечего сверх них.
Ещё две вещи задание называет прямо, иначе их не сделает никто. **Критерии
приёмки задачи, если они были, копируются в `tasks.md` отдельным блоком.**
И **записанное разведкой не переписывается второй раз**: задаче предшествовала
разведка — её записка и отвергнутые варианты уже лежат в документах канона
(`docs/research/`, `docs/adr/`), и `design.md` на них ссылается. Варианты,
разобранные без разведки (способ был очевиден, но у него оказались оттенки), — в
`design.md`, с причиной отказа по каждому отвергнутому.
**`proposal.md` пишется так, чтобы его понял человек, не читавший спек.** Это не
стилистическое пожелание: из него собирается чекпоинт шага 3, и переписывать его
там заново значит завести второй дом для одного объяснения. Требование стоит в
`openspec/config.yaml`, `rules.proposal` — то есть применяется в момент
порождения артефакта, а не вспоминается после.
### 3. Чекпоинт: объяснение
**Остановись и объясни человеку, что происходит.** Первый из двух плановых стопов
сценария, и в отличие от второго он обязателен для всякой задачи: реплика шага 6
случается только тогда, когда есть что заводить, а чекпоинт — всегда.
Он стоит **сразу после предложения и до кода** — намеренно. Раньше между
`propose` и чекпоинтом стояла стадия ревью дизайна, и человек читал объяснение,
уже просеянное машиной. Стадию сняли ради времени прогона, и просеивать теперь
нечем: человек читает предложение как оно есть. Взамен стоп пришёл **раньше**
коррекция здесь стоит правки спеки, а не переписывания готового кода.
**Это единственное место процесса, где решается форма решения, и решает её
человек.** Ревью после кода судит корректность и механику против записанного
критерия; «то ли это решение» там не спрашивает ни один проход, а глубокое ревью
области придёт позже и не всегда. Значит, чекпоинт — не формальность и не
доклад о ходе работ: одобренное здесь уезжает в код без второго суждения о
замысле.
**Объяснение не сочиняется заново — оно собирается из артефактов**, `proposal.md`
и `design.md`. Третий пересказ был бы третьим домом одного и того же и разошёлся
бы с обоими. Что показываешь:
- **в чём проблема** — словами домена из паспорта, без имён модулей и функций;
- **как решаем** — суть одним абзацем. Не пересказ дельта-спек: спека нормирует,
а здесь объясняют;
- **что человек увидит иначе**, когда это будет сделано;
- **чего мы намеренно не делаем** и почему — граница scope ловится хуже всего;
- **чем рискуем и что осталось нерешённым** — сюда съезжаются развилки,
накопленные до этого места;
- **что дальше**, если возражений нет;
- **критерии приёмки, если постановка пришла текстом и не назвала их**
предложенными, а не принятыми: человек их подтверждает или правит здесь же.
Это единственное место, где исполнитель вообще может их предложить, и работает
оно только потому, что решает всё равно человек.
Проверка на «простой язык» одна и механическая, и она общая у чекпоинтов всех
трёх сценариев — поэтому её дом здесь, а у соседей помеченные копии:
<!-- дом: чекпоинт-простой-язык -->
**в тексте нет `SHALL`, нет имён файлов и функций там, где вещь называется
по-русски, и нет слов, которых нет в паспорте проекта.**
<!-- /дом: чекпоинт-простой-язык -->
Не проходит — переписывай, а не объясняй, почему иначе нельзя.
Длина — экран. Длинное объяснение не читают, а пролистывают, и чекпоинт
превращается в ритуал одобрения.
Три исхода:
- **согласен** — идёшь на шаг 4;
- **скорректировать** — правку спек и дизайна по сказанному делает **агент**
(SKILL.md, «Кто пишет»): сказанное человеком уходит ему дословно, вместе с
идентификатором change и требованием перепрогнать
`openspec validate --strict <id>`. Затем чекпоинт **заново** — правленое
объяснение читает тот же человек;
- **не одобрено** — исход «не доведена» с причиной. Change остаётся
незаархивированным, задача не закрывается, ничего не коммитится наполовину.
### 4. Написать код — `opsx:apply`
**Код пишет агент, и в его же задании лежит весь этот раздел** (SKILL.md, «Кто
пишет»): вызов `opsx:apply` для реализации `tasks.md`, гейт до зелёного,
поведенческая верификация. Возврат — адреса тронутого, исход гейта и строка
верификации; диффа в нём нет. **Исход гейта возвращается сводкой, путём к логам
шагов и отпечатком дерева** (SKILL.md, «Возврат — не длиннее экрана»): его
передача на шаг 5 избавляет ревью от второго прогона того же гейта.
Код — по конвенциям проекта
(каталог `docs/conventions/`). Меняешь схему — обнови её описание в документации
тем же change, если проект этого требует: гейт обычно это проверяет.
Прогони гейт и добейся зелёного — он же гейт следующего шага.
**Поведенческая верификация.** Если задача меняет реальное поведение (новый
эндпоинт, разбор входа, схема, форма ответа) — зелёных юнит-тестов мало. Подними
изменение вживую командой из раздела команд `CLAUDE.md` и прогони сценарий.
Пропусти только для чисто внутренних правок без наблюдаемого рантайма.
**Сервис не оставляем лежать.** Если запуск упал — агент чинит или откатывает до
конца шага; возврат с лежащим сервисом — незакрытый шаг, а не исход.
### 5. Ревью кода — состав постоянный
Вызови Skill **`av-dev:code-review`**, дав ссылку на change `<id>`, базу диффа,
режим запуска и **исход гейта с шага 4** — сводку, путь к логам шагов и отпечаток
дерева.
**Выбирать и размечать нечего.** Состав прогона один и тот же на всякой задаче:
гейт, сверка со спекой, разбор кода, триаж; приёмник тем идёт, когда у проекта
есть свои темы. Прежде между кодом и ревью стоял отдельный проход разметки — он
считал размер по диффу, сложность по постановке и выдавал метку, из которой
выводился состав. Метка снята вместе с ним: цикл задачи проверяет корректность и
механику, а этой работе нечего добавить и нечего убавить от размера изменения.
**Режим по умолчанию — `по графу`, и обосновывать его не надо.** Конвейер сам
знает свои рёбра: гейт открывает проходы с мнением, триаж — сток. Просить
**`линейно`** нужно только по причине, и она называется строкой: так сказал
оператор; машина занята чем-то ещё; идёт разбор самого конвейера.
Скилл сам гоняет гейт, нужные проходы и обязательный триаж. Возвращает отчёт с
потолком 7 пунктов, разметкой `Действие: инлайн | развилка`, секцией `Урожай`,
секцией отложенного в глубокое ревью и границами покрытия.
**Сверь перечень тем с исходом, прежде чем коммитить.** Отчёт начинается таблицей
«тема → кто закрывает → против чего», и против каждой темы обязан стоять исход.
Тема без отчёта и тема без дома — разные вещи, и обе должны быть названы.
Реестр постоянный и короткий, сверка стоит одного взгляда.
#### Отработка — чинится молча, спрашивается редко
Помеченное `инлайн` чинит **агент** (SKILL.md, «Кто пишет»): находки уходят ему
**дословно, вместе с оракулом**, одним заданием на весь урожай инлайна, и гейт
после правок гоняет он же. Логировать их не надо. **Это умолчание, и оно
широкое** — прогон, вернувший человеку список замечаний вместо готового
результата, свою работу не сделал.
`развилка` — вопросом в запись (он уже сформулирован триажем, его остаётся
перенести), и агенту она не отдаётся. Оснований у неё три, и все узкие: правка
меняет **дельта-спеки**, находка сидит в **необратимом** месте (миграция, формат
на диске, публичный контракт), находка трогает **инвариант** `CLAUDE.md`.
Развилок больше двух на задачу — это факт для доклада: либо задача не та, либо
разметка действий съехала.
**Находка, отменяющая одобренный дизайн, отменяет и одобрение.** Признак
проверяемый: **меняются ли дельта-спеки**.
- не меняются — находка внутри дизайна, дожимай сам, это обычная отработка;
- меняются — решение стало другим, а одобрено было прежнее. **Вернись на чекпоинт
шага 3** с тем, что изменилось и почему; дальше задача идёт своим ходом заново —
код, ревью. Такая находка агенту не отдаётся ни при каких условиях: она отменяет
одобрение, а это разговор с человеком.
**Это правило старше правила о развилке.** Находка класса `развилка`, чьё
основание — «надо менять спеку», подпадает под оба; побеждает возврат на
чекпоинт, а вопрос в запись при нём остаётся, но возврата не заменяет. Иначе
одобренный дизайн переделывался бы записанным вопросом, то есть молча.
Тихо переделать утверждённое нельзя. Чекпоинт, который можно обойти находкой
ревью, не значит ничего, а человек при этом уверен, что одобрил именно то, что
уехало в коммит.
#### Урожай — список в докладе, задачи только по слову человека
Отложенные находки (реальный `major` не для этого мерджа, развилка, решённая
«потом», пачка `nit`) собери в секцию доклада `Урожай`: формулировка, оракул,
откуда взялась.
**Задачи из урожая заводятся только тогда, когда человек сказал «заводим».**
Спрашивается это **не здесь, а на шаге 6** — там же, где спрашивается новое в
документах, и той же одной репликой: два вопроса подряд про одно и то же («что из
найденного заводим») стоили бы человеку двух переключений вместо одного. Сюда
урожай складывается, а не выносится.
Сказал «заводим» — зовёшь `av-dev:task-track` **ты сам**, тактом третьим шага 6:
у него на этот вход отдельный сценарий «задачи из ревью и аудита» — своя нарезка,
свой формат, свои правила дублей, и находка передаётся дословно. Не сказал —
урожай остаётся строками доклада, и это исход, а не потеря.
**Молча беклог не наполняется.** Очередь работ ведёт человек, и задача, заведённая
за него по ходу чужого прогона, отнимает у него ровно то решение, ради которого
очередь и существует. Прежде вызов `av-dev:task-track` был обязательным шагом —
теперь он шаг по ответу.
**Границы покрытия из отчёта не выбрасывай** — они уезжают в финальный доклад
сжатой строкой. Отчёт, из которого исчезло «что проверить было невозможно»,
превращается в ложное ощущение проверенности.
**Строки «отложено в `av-dev:code-deep-review`» перенеси дословно.** Их пишут
проходы, упёршиеся в предел цикла: нужен замер, нужен прогнанный путь, нужен вход
шире диффа. В цикле задачи это не доказывается ничем, а строки копятся и однажды
становятся поводом позвать глубокое ревью области; пересказанные своими словами,
они теряют оракул и перестают быть поводом.
**Сигнал «это изменение просит глубокого ревью»** приходит от `review-code` и
подтверждается `review-basics`. Он не команда и не стоп — строка доклада: когда
звать глубокий прогон, решает человек.
**Отчёт триажа сохрани вместе с change (`openspec/changes/<id>/review/`; шаг 6
унесёт его в архив вместе с change) — это обязательно, а не «если удобно».** По
нему потом видно, что было найдено и что из этого осталось в урожае. И это
единственный **независимый** артефакт о составе прогона: своей прозе здесь верить
нельзя — её написал тот, кто мог проход и пропустить.
### 6. Архивация и документы — отражение молча, новое по слову
Шаг идёт **в три такта**, и агент запускается в нём дважды. Причина одна: письмо
в документы бывает двух родов, а спрашивается только один.
**Копия.** Дом правила — раздел «Два рода правок» скилла `av-dev:doc-sync`.
Правится дом, а не этот файл.
<!-- копия: синк-род-правки из av-dev/skills/doc-sync/SKILL.md -->
- **Отражение** — документ уже описывает эту вещь, и без правки он **станет
ложным**: миграция написана, а `database.md` её не знает; компонент заведён, а
`architecture.md` перечисляет прежние. Такая правка ничего не решает, она
договаривает решённое на чекпоинте и уже стоящее в коде. **Пишется молча.**
- **Новое** — в каноне заводится запись или норма, которой не было: ADR, правило
в конвенциях, записка в `research/`, инвариант в `CLAUDE.md`, сдвиг периметра в
`security.md`, граница в `passport.md`, дефект в журнале `review.md`. Такая
запись переживёт задачу и свяжет следующие. **Пишется только по слову
человека.**
<!-- /копия: синк-род-правки -->
#### Такт первый — агент: архив и отражение
**Оба скилла уходят одному агенту, и это один запуск** (SKILL.md, «Кто пишет»).
Работа письменная от начала до конца: `opsx:archive` вливает дельты в актуальные
спеки, `av-dev:doc-sync` идёт по чек-листу и пишет отражение, и обе правки — по
чек-листам своих скиллов, а не по суждению оркестратора. Разнесённые по двум
запускам, они стоили бы двух заданий, двух возвратов и паузы между ними — при
том что второй читает ровно то, что оставил первый.
В задании: корень проекта, идентификатор change, база диффа и **порядок**
сначала `opsx:archive` с `openspec validate --strict` перед ним, затем
`av-dev:doc-sync`.
**Вычитка и гейт идут последним тактом, в котором писали.** Вернул непустой
список предложений — оба ждут третьего такта; список пуст — этот такт последний,
и оба идут в нём. Гонять гейт дважды подряд по одному дереву незачем, а вычитывать
пачку, которая сейчас пополнится, — тем более. Вычитку зовёт сам
`av-dev:doc-sync` (агента `doc-wording` по пачке правленого), и правило живёт в
том скилле; гейт до зелёного доводит агент, потому что красный гейт остановил бы
коммит следующим шагом — документы у многих проектов он проверяет.
**Отложенное этим тактом обязано вернуться.** Оттого такт третий идёт **всякий
раз, когда была реплика** — в том числе когда человек не одобрил ничего: на нём
висят вычитка и гейт, которые первый такт с себя снял. Пропустить его на отказе
значило бы уехать в коммит с невычитанной правкой и непрогнанным гейтом.
**Возврат — чек-лист, адреса тронутого, исход валидации, строка сигнала сверки и
исход гейта, если он гонялся.**
Чек-лист уезжает в доклад целиком, и переписывать его своими словами нельзя —
это единственный след того, что каждый документ был назван.
**Правило, которое задаёт его форму, одно и оно жёсткое: принуждённое
отрицание.** Против **каждого** документа канона стоит одно из трёх — чем он
обновлён, что по нему предлагается, либо «не требуется, потому что…».
Нетронутые группируются одной строкой с общей причиной. Список триггеров прозой
уже проверен на живом проекте и дал 6 записей ADR на 43 изменения; работает
только обязательное отрицание. **Требование стоит в задании агента** — без него
возврат придёт перечнем тронутого, а тронутое без нетронутого не отличается от
невыполненного шага.
**Список документов и их триггеров здесь не дублируется** — он в чек-листе скилла
`av-dev:doc-sync`, и копия уже однажды разошлась с оригиналом, потеряв два
триггера.
**Документов канона в проекте нет** — синка нет вовсе: назови это исходом и
предложи завести канон скиллом `av-dev:canon`. Придумывать раскладку под
задачу нельзя: документ, заведённый мимо канона, окажется вторым домом ровно
тому, что канон потом заведёт своим.
#### Такт второй — одна реплика человеку на весь хвост
Покажи **одним списком** всё, что заводится нового:
- **предложения синка** — ADR, конвенция, записка в `research/`, инвариант,
периметр, дефект в журнал. Каждое строкой: что заведём, куда и на каком
основании;
- **урожай ревью с шага 5** — отложенные находки, из которых получаются задачи:
формулировка, оракул, откуда взялась.
Человек отвечает разом. **Нового нет — реплики нет**, и шаг кончился первым
тактом; у большинства задач так и выходит.
**Реплика одна, и делить её нельзя.** Спросить про ADR на синке, а про задачи
отдельно — значит взять с человека два переключения там, где решение одно: что из
найденного этой задачей переживёт её. Ровно поэтому вопрос про урожай и перенесён
сюда с шага 5.
**Спрашиваешь, а не советуешь по каждому пункту.** Основание уже названо строкой,
и второй абзац уговоров превращает реплику в чтение. Человек вправе ответить
«ничего» — это исход, а не потеря: находки остаются строками доклада.
#### Такт третий — задачи оркестратором, документы агентом
**Идёт всякий раз, когда была реплика**, и порядок в нём жёсткий.
**Сначала задачи — их заводишь ты, а не агент.** Человек сказал «заводим» — зови
Skill `av-dev:task-track`, у него на этот вход отдельный сценарий «задачи из ревью
и аудита»: своя нарезка, свой формат, свои правила дублей. Находка передаётся
**дословно, вместе с оракулом**. Согласован промоут находки в конвенцию — тем же
вызовом заводится **задача `chore` на механизацию правила**: шаг 2 промоута
(конфиг линтера, сканер, приведение кода к зелёному) в хвост чужой задачи не
помещается (`av-dev:code-review`, `references/promote.md`).
**Заведение задач агенту не отдаётся ни в одном сценарии** — по той же причине,
по какой ему не отдаются коммит и закрытие: оно правит индексы учёта, а перечень
работ ведёт человек. Правило и его дом — SKILL.md, «Кто пишет».
**Потом документы — их пишет тот же агент, что шёл тактом первым.** В задании:
- **одобренные записи дословно** — формулировка, источник, основание; сочинять
заново нельзя, ADR цитирует решение из архивного `design.md`, а не пересказывает
его. Человек не одобрил ничего — писать нечего, и это законный вход;
- **вычитка** `doc-wording` по всей пачке правленого — и первого такта, и этого;
- **гейт проекта до зелёного** после правок — он же увидит заведённые задачи,
потому они и заводятся раньше.
**Отвергнутое не пишется никуда.** Ни в один документ, ни отдельной записью «от
такого-то отказались»: журнала отвергнутого канон не держит, и заведение его
здесь было бы ровно тем новым, которого человек только что не заказал. Отказ
идёт строкой доклада.
### 7. Коммит
Коммить **в текущую ветку** (`git rev-parse --abbrev-ref HEAD`), сам ветку не
создавай и не переключай, ничего не пушь.
Сообщение — по-русски, **скиллом `av-dev-git:commit`**: форму сообщения держит
он, и здесь она не пересказывается. Вызов не разрешился — плагина в проекте нет:
напиши сообщение сам и скажи строкой доклада, что форму коммита не сверял никто.
Одна задача — один осмысленный коммит.
### 8. Закрыть задачу — **после коммита, не раньше**
**Вызови Skill `av-dev:task-track`** и попроси закрыть задачу как реализованную —
он владеет форматом и двигает строку индекса сам. Путь к его скрипту не выясняй и
индексы руками не правь: мост между плагинами — вызов скилла, а не путь.
**Порядок обязателен.** Закрытие удаляет файл задачи; сделанное до коммита оно
оставило бы задачу закрытой без единого следа работы, если шаг 7 упадёт.
**Закрытие тоже коммитится — вторым коммитом, тут же.** Удаление файла задачи и
правка индексов (их имена знает `av-dev:task-track`, не ты) — это правки в рабочем
дереве, и оставить их незакоммиченными нельзя: закрытие, не доехавшее до основной
ветки, оставит задачу открытой молча, а опора «индексы под git показывают, что и
когда закрыто» без коммита — пустые слова. Сообщение короткое, про учёт, а не про
работу: `закрыта задача <slug>`. Это второй коммит осознанно: правило «одна задача
— один осмысленный коммит» про работу, а учёт — не работа.
**Постановка пришла текстом — шага нет вовсе, и это не пропуск.** Записи не
существовало, закрывать нечего, а следом работы служат коммит и заархивированный
change. Заводить запись задним числом, чтобы её тут же закрыть, нельзя: учёт
получил бы задачу, которой никто не ставил, и закрытие без единой минуты
открытого состояния. Скажи это строкой и переходи к докладу.
Каталога задач в проекте нет — **ничего не выдумывай**: скажи в докладе, что
учёт задач остаётся за владельцем, и назови исход.
## Доклад решения
Общее ядро доклада — в SKILL.md; сверх него сценарий решения обязан назвать:
- **что было одобрено на чекпоинте и совпало ли с тем, что уехало в коммит**
расхождение здесь называется прямо, даже если оно мелкое;
- ссылка на архивный change и хеш коммита;
- по каждому критерию приёмки, если они были: **оракул и наблюдаемый исход**
это доклад приёмщику, а не отметка «принято»;
- **`Урожай`** — отложенные находки списком (формулировка, оракул, откуда
взялась) и **что человек по нему решил**: заведены задачи или список остался в
докладе;
- **что заведено нового в документах** — одобренное по именам записей, и **что
предложено и отвергнуто**, тоже по именам. Отказ виден только здесь: в
документы он не пишется;
- **сигнал сверки** — строка синка о том, сколько задач сделано с прошлого
прогона `av-dev:doc-healthcheck`, либо что сверки не было ни разу;
- **сколько находок ушло инлайном и сколько развилкой** — числом. По нему видно,
во что прогон обошёлся человеку;
- **одна строка границ покрытия**: какой режим гонялся, какие проходы не
запускались и что проверить было невозможно;
- **отложенное в `av-dev:code-deep-review`** — дословно из отчёта, либо «нечего». Доклад без неё сообщает
«проверено», не сообщая, что именно.
## Тонкости сценария
- Гейт блокирует: пока он красный, проходы с мнением не запускаются. Чинить и
перезапускать, а не «посмотреть заодно».
- Стиль правок — заточка под проект и конвенции, по размеру задачи, без
улучшений заодно.
- **Пропустить тему или проскочить чекпоинт — самый дешёвый способ «ускориться»,
и он же самый дорогой по последствиям.** Защита устроена так, что регулятора у
тебя нет: состав прогона постоянный и сокращению не подлежит, перечень тем
сверяется по исходу, непокрытое называется строкой, а расхождение с одобренным
— отдельным пунктом доклада.
- **Заведение задач из урожая ревью не идёт по умолчанию.** Отложенные находки
отдаются **списком**, и в задачи их превращает `av-dev:task-track` — по слову
человека и вызовом от тебя, а не от агента: перечень работ ведёт человек, а
индексы учёта правит тот же, кто коммитит. У скилла на этот вход отдельный
сценарий «задачи из ревью и аудита». Каталога задач в проекте нет — урожай
остаётся списком в докладе, и это говорится строкой.
- **Стопов у сценария два, и оба про решения человека, а не про ход работ.**
Чекпоинт шага 3 решает форму решения **до** кода; реплика шага 6 решает, что из
найденного переживёт задачу. Между ними прогон идёт сам: правки инлайном чинятся
молча, отражение в документах пишется молча. Третьего стопа заводить нельзя —
прогон, останавливающийся чаще, теряет ровно то время, ради которого короткие
итерации и выбраны.
- **Способ решения ты не выбираешь.** Он приходит известным: из постановки, из
разведки, от человека. Выбор между двумя подходами с разной ценой делается в
разведке, у своего чекпоинта, — не по ходу этого сценария.
+993
View File
@@ -0,0 +1,993 @@
---
name: code-review
description: "Конвейер ревью изменения, устроенный по темам: документ проекта либо заводит тему ревью, либо питает чужую тему источником, либо процессный и в ревью не читается вовсе. Ядро тем — requirements, autotests, conventions, architecture, security, operations; список тем открытый, свои темы проект заводит документом. Состав прогона постоянный, метки у него нет: гейт (autotests), сверка со спекой (specs), разбор кода и конвенций (code), триаж; приёмник тем (basics) идёт, когда у проекта есть свои темы. Цикл задачи проверяет корректность и механику против записанного критерия — дельта-спеки, конвенции, инварианты CLAUDE.md, вывод инструментов. Темы риска и устройства — security, operations, architecture — закрыты в цикле только сверкой с записанными инвариантами: их разбор, доказательство запуском и суждение о форме решения живут в скилле av-dev:code-deep-review, который идёт по области кода и время от времени. Порядок прогона — граф зависимостей: гейт открывает проходы с мнением, триаж — единственный сток. Находки по умолчанию чинятся инлайн и молча; человеку уходит только необратимое, трогающее инвариант CLAUDE.md и меняющее дельта-спеки, а задачи из урожая заводятся по его слову. Проектная специфика приходит из документов канона проекта. Вызывается из скилла av-dev:code-resolve после apply. Второй вызов идёт от сценария обслуживания: без change, фиксированным планом (autotests, operations, плюс conventions, если тронут код)."
---
# Конвейер ревью
Готовит ревью — **не заменяет его**. Потребитель отчёта — оркестратор, который
чинит код; человек читает только сводку, развилки и границы покрытия.
## Четыре правила, из которых всё следует
Если ситуация не покрыта инструкцией — решай по ним.
0. **Тема первична, проход вторичен.** Ревью проверяет **темы** — набор
направлений, который проект объявляет своими документами. Проход это только
способ закрыть тему на заданной глубине, и проходы меняются: переезжают в
другой скилл, сливаются, упраздняются. Если состав прогона считать списком
проходов, то уехавший проход уносит тему с собой **беззвучно** — отчёт честно
скажет «`ops` не запускался» и не скажет «эксплуатацию не смотрел никто», а
нужно второе. Проверено на живом переезде: `ops` и `adversary` ушли в
`av-dev:code-deep-review`, а темы `security` и `operations` остались в
конвейере — узко, сверкой с инвариантами внутри `code`, и это записано
строкой. Поэтому прогон описывается таблицей «тема → кто закрывает → против
чего», и таблица эта есть в каждом отчёте.
1. **Recall чек-листа равен длине чек-листа.** Проход, устроенный как «проверь
пункты 1..N», найдёт ровно перечисленное. Всё неявное — идиомы, форма
решения, «так не делают» — неперечислимо по определению: перечислимое уже
стало бы конвенцией. Отсюда деление проходов на **applicative** (применяют
заданный критерий) и **generative** (сперва порождают критерий или
альтернативу, потом сравнивают). Расширять чек-листы бесполезно; неявный слой
достают только generative-проходы.
2. **Ценность верификатора = наличие внешнего оракула × разведённость с
автором**, а не число ролей. Под всеми ролями одна модель с одними
априорными, вход у всех общий: седьмая роль почти не добавляет recall, но
линейно удорожает триаж. Иерархия надёжности: детерминированный инструмент >
агент, который его **запускает** и интерпретирует вывод > агент с чистым
мнением. Максимум работы переносим вниз.
3. **Отчёт без границ покрытия хуже отсутствия отчёта.** «Критичных проблем не
обнаружено» потребляет ощущение проверенности, ничего не гарантируя. Секция
границ покрытия обязательна и не сокращается — в том числе в докладе человеку.
## Предпосылки
Конвейер опирается на внешнюю обвязку и без неё работает не целиком. Проверь это
один раз, при установке плагина в проект:
- **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`,
упадут на «нет такого скилла», а `review-specs` останется без источника
требований. **Проект без OpenSpec этим конвейером не проверяется** — подключай
OpenSpec, а не понижай прогон: ветка деградации здесь не пишется, потому что
непроверенная ветка деградации хуже честного отказа. Заводить руками не надо:
этим владеет скилл `av-dev:code-openspec` — он заводит каталог и заменяет
пример в `config.yaml` настройкой. Его же зовут `av-dev:doc-init` на новом
проекте и `av-dev:canon` в режиме `adopt` — на переводимом.
**Предпосылка эта — про изменение поведения, а не про всякий прогон:**
сценарий обслуживания зовёт конвейер без change и без дельта-спек, и ни один
проход его плана на них не завязан. См. «Прогон без change».
- **Документы канона** — см. следующий раздел.
- **Проектные копии этих скиллов и агентов удаляются при установке.**
<!-- копия: проектные-копии из README.md -->
Голые имена в `.claude/skills/`: `resolve`, `review`, а у проектов прошлого
поколения ещё `task-pipeline`, `review-pipeline`, `task-batch`. С префиксом
проекта: `<проект>-task-pipeline`, `<проект>-review-pipeline`. Агенты:
`.claude/agents/<проект>-review-*.md`.
Две копии одного скилла расходятся, и побеждает та, что **короче названа**:
короткое имя разрешится в устаревшую проектную копию молча и без признаков
подмены.
<!-- /копия: проектные-копии -->
### Чего может не быть
**Копия.** Дом правила — `shared/absence.md` в репозитории плагина.
Правится дом, а не этот файл.
<!-- копия: отсутствие из av-dev/shared/absence.md -->
**Скилл не вправе считать раскладку проекта полной.** Части заводятся порознь и
живут порознь; каждая узнаётся своим следом:
| Чего нет | Как видно | Чего теперь не делает никто |
| --- | --- | --- |
| настройки av-dev | нет `.av-dev.toml` в корне | проект под процесс не заводился; версии нет, настроек нет |
| документы канона | нет `docs/` | проектную конкретику брать неоткуда — темы, инварианты, прецеденты |
| учёт работ | нет каталога задач | запись остаётся владельцу: назови её текстом в докладе |
| источник требований | нет `openspec/config.yaml` | цикл SDD не запускается: спеки не с чем сверять |
**Свой скилл зовётся полным именем** — `av-dev:canon`, `av-dev:task-track`,
`av-dev:code-review`. Короткое имя может разрешиться в устаревшую проектную
копию из `.claude/skills/`, и подмены не будет видно ни в докладе, ни в
поведении.
**Внешний плагин может не стоять.** Их два: `opsx:*` — цикл SDD, и
`av-dev-git:commit` — сообщения коммитов. Путь в дерево чужого плагина не
пишется никогда: `$CLAUDE_PLUGIN_ROOT` ведёт только в своё дерево, а
вычисленный от него путь к соседу либо не откроется, либо откроет чужую
установку. Нужен чужой справочник — зови владеющий им скилл, он прочитает его
сам.
**Отсутствие — исход, а не поломка.** Назови строкой доклада, чего теперь не
делает никто, и продолжай работу. Молчать нельзя: пропуск неотличим от
сделанного. Выдумывать обходной путь нельзя тоже.
**Присутствие узнаётся следом в проекте, а не объявлением.** Перечня того, что
здесь заведено, проект не ведёт — он разошёлся бы с действительностью молча.
<!-- /копия: отсутствие -->
Своих скиллов это касается ровно так же: `av-dev:code-review`,
`av-dev:code-resolve`, `av-dev:code-openspec` — подменяется короткое имя,
а не чужое.
## Темы, источники и процессные документы
Раньше здесь стояло плоское правило «каждый документ проекта — тема ревью». Оно
верно ровно наполовину, и потому вредно целиком: паспорт и схему хранилища
ревью читает, но темами они не являются, а журнал решений и журнал наблюдений
ревью изменения не нужны вовсе. Прогон, применявший правило буквально, обязан был
либо завести фантомные темы и продублировать ими работу настоящих, либо потерять
документ молча.
**Разрез один и проверяемый — тот же, что в каноне: можно ли по документу
сказать «в этом изменении сделано не так»?**
| Категория | Что конвейер с ней делает | Кто в ней |
|---|---|---|
| **тема** | заводит направление проверки и требует исполнителя | `conventions.*`, `security.*`, `architecture.*`, свои документы проекта |
| **источник темы** | читает как материал чужой темы, своей не порождает | `passport.*`, `database.*`, `CLAUDE.md`/`AGENTS.md`, `openspec/specs/` |
| **процессный документ** | не судит по нему изменение | `tasks/`, `docs/review.*`, `docs/adr.*`, `docs/research.*`, `.av-dev.toml` |
**Одна процессная запись всё же читается — `docs/review.*`.** В ней лежит
настройка самого конвейера: вопросы по темам, журнал дефектов, типовые узлы,
типовые ложноположительные. Проход, читающий её, читает **свою обвязку**, а не
критерий, по которому судит изменение. `adr.*`, `research.*` и `tasks/` не
открывает никто.
Дом канона этой раскладки — скилл `av-dev:canon`, раздел «Три категории
документов». Конвейер её **читатель**: категории и имена тем он берёт
оттуда и своих не заводит.
Отсюда то, ради чего правило и заведено: **`docs/` перестаёт быть просто
документацией и становится конфигурацией конвейера**. Проект настраивает ревью
тем, что пишет о себе, а не отдельным файлом настроек, который разошёлся бы с
документами. **Открыта при этом только категория `тема`** — две другие закрыты
и перечислены поимённо, поэтому документ, которого нет в раскладке канона,
однозначно своя тема проекта, а не «что-то непонятное».
Ядро — шесть тем, они есть у любого проекта, приведённого к канону. Форма дома
значения не имеет: `docs/security.md` и `docs/security/` — одна тема `security`.
| Тема | Дом | Вопрос темы |
|---|---|---|
| `requirements` | `openspec/specs/`, дельты change | делает ли код заказанное, и только его |
| `autotests` | `CLAUDE.md`: семантика гейта, команды | проверено ли машиной и хватает ли проверок |
| `conventions` | `docs/conventions.*` | написано ли так, как здесь пишут |
| `architecture` | `docs/architecture.*` + источник `passport.*` | цело ли устройство: понятия и границы |
| `security` | `docs/security.*` | что сделает недоверенный вход |
| `operations` | `docs/architecture.*`, раздел эксплуатации + источник `database.*` | что будет через неделю на проде |
**Три темы ядра дома в `docs/` не имеют, и это не пробел.** `requirements` живёт
в `openspec/`, `autotests` — в `CLAUDE.md`, `operations` — разделом внутри
`architecture.*`. Имя темы поэтому не выводится из имени файла, и обратно тоже:
`docs/passport.md` не заводит темы `passport`.
**`adr/` и `research/` прогон больше не открывает.** Раньше архитектурный проход
читал решения, а эксплуатационный и `specs` — числа. Цена решения записана в
каноне и повторяется здесь, потому что платит её конвейер: **расхождение
изменения с записанным решением прогоном не ловится**, это работа сверки
документации — скилл `av-dev:doc-healthcheck`.
Строка об этом обязательна в границах покрытия каждого прогона.
**Своя тема проекта бывает двух происхождений, и обе законны:** документ, который
проект положил в `docs/` и которого нет в раскладке канона, — и **тема, названная
директивой** `CLAUDE.md`/`AGENTS.md`, у которой документа нет вовсе. У второй дом
— сама директива; в остальном она ничем не отличается, и в раздаче идёт туда же.
Различать их приходится потому, что условие запуска приёмника тем звучит «есть ли
свои темы проекта», и тема без файла в `docs/` иначе не попала бы под это условие
никогда.
**Проектная тема закрывается `basics`**, и только она. Именных проходов конечное
число, а тем — сколько заведёт проект; приёмник обязателен, иначе открытость
списка была бы обещанием без механизма. Темы **ядра** он не держит **в цикле
задачи** — на прогоне обслуживания план сценария даёт ему `operations`, и это
единственное исключение (раздел «Прогон без change»). В цикле:
`requirements` закрывает `specs`, `conventions` и технику — `code`, а риск и
устройство — тот же `code` сверкой с инвариантами. Отсюда правило состава:
**`basics` запускается тогда и только тогда, когда ему есть что принимать** — см.
«Состав прогона».
**Тема без дома — законное состояние и отдельная строка.** «Тема `operations`
заявлена, `docs/database.md` нет» читается иначе, чем «не смотрели». Деградация
поразрядная: нет дома — падает глубина этой темы, и только её.
Что именно проход читает по каждой теме — [references/project-facts.md](references/project-facts.md).
Отдельного файла-брифа при этом нет: пути известны, посредник не нужен, а второй
дом для тех же фактов разошёлся бы и выглядел актуальным.
**Документов канона нет вовсе** — проект не приведён к канону. Скажи это строкой
и предложи скилл `av-dev:canon`: одна операция на проект против деградации на
каждой задаче. Прогон при этом не останавливается.
## Что получает каждый проход
Задание собирается **по таблице тем** и состоит из шести вещей:
- **его темы** — какие темы он закрывает, у каждой **дом** (путь и раздел) и
**глубина**. Дом передаётся адресом, а не пересказом: проход, получивший
проинтерпретированный периметр, не заметит, что интерпретация неверна;
- **вопросы по его темам** из `docs/review.*`, если они там есть, — **дословно**.
Вопрос привязан к теме, а не к имени прохода, и потому переживает переезд
прохода между скиллами;
- **контракт находок** — путь к
[references/finding-contract.md](references/finding-contract.md) (в
установленном плагине — `${CLAUDE_PLUGIN_ROOT}/skills/code-review/references/`);
- **изменение** — идентификатор change и путь к его дельта-спекам;
- **база диффа**;
- **его глубина и режим** прогона — чтобы проход знал, что писать в границы
покрытия.
**Ступень 1 получает сверх этого исход гейта, прогнанного до ревью** — сводку,
путь к логам шагов и отпечаток дерева, — если вызывающий скилл его дал. Зачем и
что происходит при расхождении — «Ступень 1 — Автотесты».
Чего проход **не** получает ни в каком режиме — выводов других проходов. См.
«Порядок прогона».
## Модель по проходу
Модель выбирается **по цене ошибки прохода, а не по его роду**. Признак рабочий и
проверяемый: находка со ссылкой на записанный источник — строку спеки, цель в
манифесте, значение в конфиге — опровергается открытием файла, и дешёвая модель
ошибается здесь проверяемо; находка-суждение опровергается рассуждением, а
рассуждение стоит триажа или человека. Второй род ошибки — **пропуск**: он не
стоит ничего сегодня и не виден вовсе, и проход, у которого дороже пропустить,
держится наверху, даже будучи applicative.
Модель задана во frontmatter каждого агента, менять её здесь не нужно.
| Модель | Цвет | Проходы | Почему |
|---|---|---|---|
| `sonnet` | green | autotests, ops | вывод перечислим и сверяется механически |
| `opus` | yellow | specs, code, basics, triage, rubric, adversary, architecture | дорога ошибка — ложная либо пропущенная |
**В таблице стоят и проходы, которых в цикле нет.** `adversary`, `ops` и
`architecture` работают в скилле `av-dev:code-deep-review`, `rubric` зовут прямо
руками; раскладка «модель — цвет» общая для всех уставов плагина и проверяется
механически, поэтому дом у неё один, а не по скиллу.
**Цвет charter'а кодирует модель, а не роль прохода.** Это единственное
назначение цвета: список агентов читается взглядом, и по нему сразу видно, чем
платит прогон. Роль прохода из имени и так понятна, а цвет, розданный по ролям,
не отвечает ни на один вопрос, который задают во время прогона. Раскладка живёт
здесь и **проверяется механически** — цвет ставится один раз при заведении
charter'а, а модель потом двигает калибровка, и разъезжаются они молча.
**Моделей две, и верхняя из них — `opus`; выше неё конвейер не платит.** Замер:
на первом же прогоне самые ценные находки дали `opus`-проходы — сверка спек дала
13 находок с оракулами, а проход про идиоматичность (впоследствии упразднённый) —
три эксперимента против драйвера БД с воспроизведёнными числами. Разницы в пользу
модели **дороже** `opus` не обнаружилось ни на одном проходе, а прогон на ней
стоил заметно дольше и дороже — значит платить за неё не за что.
Четверо держатся наверху не за суждение, а по отдельным причинам, и их стоит
знать поимённо:
- `triage` — через него проходит всё, что оркестратор реализует **молча**:
ложноположительная находка становится кодом, потерянный `critical` — дефектом.
Ошибка триажа дороже ошибки любого отдельного прохода.
- `specs` — по устройству applicative, но направление `code → spec` требует
заметить **отсутствие**: тихий фолбэк, самодеятельный дефолт, проглоченную
ошибку. Здесь дорог пропуск, а не ложная находка.
- `code` — единственный, кто читает код **как код**, и с уходом тяжёлых проходов
он же единственный, кто смотрит на риск и устройство. Его пропуск это дефект в
проде, и он не оставляет следа ни в отчёте, ни в границах покрытия. По той же
причине, что `specs`, и это дороже всего в конвейере: проход идёт на каждой
задаче.
- `basics` — держит темы, которые проект завёл сам, то есть ровно те, о которых
плагин ничего не знает. Ошибиться на чужой теме дешёвой моделью проще всего:
критерий приходит текстом документа, а не перечнем.
**Самая дешёвая модель не используется ни на одном проходе, и это не экономия
наоборот.** Дешёвая модель на проходе с мнением даёт правдоподобные находки,
которые триаж обязан опровергать оракулом, — а это самая дорогая операция
конвейера. Механизируемая же работа здесь вынесена **ниже** модели: гейт,
покрытие диффа, карта проекта — это скрипты проекта, они стоят ноль токенов.
Дешёвому проходу просто не осталось работы.
Экономия достигается **не понижением модели, а тремя другими рычагами**, и все
три применяются к каждому проходу с мнением, а не к одному избранному.
1. **Непуск.** Тяжёлые проходы в цикле не запускаются вовсе — они живут в
`av-dev:code-deep-review`; приёмник тем не идёт, когда своих тем у проекта
нет. Что при этом перестаёт проверяться, названо поимённо и идёт в границы
покрытия.
2. **Вход.** `basics` идёт на верхней модели, но с узким входом: дифф и его
окрестности, без карты проекта. Карта проекта и вход шире диффа не даются в
цикле никому — это цена глубокого прогона, а не задачи.
3. **Потолок.** Он есть у каждого прохода с мнением и напечатан: `basics` — 4
находки; `code` — 4 конвенционных и 1 на все три темы риска и устройства
разом, у технической половины потолка нет; `specs` — потолка нет; триаж — 7 в
основном списке. Двум половинам его не ставят намеренно: пропуск дефекта и
пропуск расхождения со спекой стоят дороже длинного списка.
Все три раньше зависели от метки и потому на каждой задаче считались заново.
Теперь они постоянные, и проход знает свой потолок до того, как получил задание.
Проход без потолка выдаёт столько находок, сколько нашёл поверхностей, — а это
ровно тот механизм, из-за которого был снят проход независимой реализации:
**счёт определялся объёмом вывода**. Потолок ставится не ради краткости отчёта, а
против этого.
**Потолок обязан быть объявлен, когда он сработал.** Проход, срезавший находки
до потолка, говорит об этом строкой в своих границах покрытия: сколько осталось
за срезом и какого рода. Молчащий срез неотличим от «больше не нашлось» — это тот
же класс молчащего пропуска, что и непущенный проход.
## Состав прогона — постоянный
**Ступени нумерованы, и наружу выходит одна.** Прогон ревью один, и зовёт его
`av-dev:code-resolve` после того, как код написан; членение внутри прогона —
ступени, и знать их снаружи не нужно. Исключение единственное и названное:
**ступень 1**, автотесты, — на неё ссылаются снаружи, потому что она умеет
засчитать чужой прогон гейта по отпечатку дерева, и вызывающему надо знать, куда
этот отпечаток едет. Перечень осей процесса целиком —
[shared/axes.md](../../shared/axes.md).
**Состав не выводится ни из чего: он один и тот же на всякой задаче.** Гейт,
сверка со спекой, разбор кода, триаж; приёмник тем — когда у проекта есть свои
темы. Прежде состав выбирала **метка** `small`/`medium`/`large`, которую считал
отдельный проход по двум осям — размеру и сложности. Метки больше нет, и вместе с
ней ушли разметка, матрица выбора, правило «спорное решается вниз» и доли по
журналу.
**Цикл задачи проверяет корректность и механику, и это его определение.**
Вопрос «делает ли код заказанное и не сломается ли он сам» отвечается против
**записанного** критерия: дельта-спеки, конвенции, инварианты `CLAUDE.md`, вывод
инструментов. Вопрос «то ли это решение» здесь не задаётся вовсе: он стоит
человеку разговора, а место разговора назначено — чекпоинт до кода, где форму
решения одобряет человек, и скилл `av-dev:code-deep-review`, где находки
разбирают по одной.
Отсюда таблица тем — единственная и без вариантов:
<!-- дом: тема-глубина -->
| Тема | Кто закрывает | Против чего и как |
|---|---|---|
| `autotests` | `autotests` | запуск: гейт проекта и логи его шагов |
| `requirements` | `specs` | разбор: дельта-спеки change, сверка в обе стороны |
| `conventions` | `code` | разбор: дома конвенций проекта |
| техника | `code` | разбор: дефект, который сработает сам |
| `security`, `operations`, `architecture` | `code` | **сверка с записанными инвариантами `CLAUDE.md`** — и только |
| тема проекта | `basics` | разбор: дом темы против диффа |
<!-- /дом: тема-глубина -->
**Три темы риска и устройства закрыты узко, и это названо прямо.** Свойство,
которого нет в инвариантах, в цикле не спросит никто: ни сценарием, ни чтением
дома темы. Это самая крупная граница покрытия конвейера, она идёт строкой в
каждом отчёте, и снимает её не прогон задачи, а глубокое ревью области.
Весь процесс с исполнителями — одной схемой:
```mermaid
flowchart TD
propose["opsx:propose — change, дельта-спеки, tasks.md"]
checkpoint(["чекпоинт: форму решения одобряет человек"])
apply["opsx:apply — код, гейт зелёный"]
subgraph code["Ревью кода — состав постоянный"]
cGate["autotests — гейт, источник графа"]
cS["specs — requirements"]
cC["code — conventions, техника<br/>и сверка с инвариантами:<br/>security, operations, architecture"]
cB["basics — только свои темы проекта"]
cT["triage — единственный сток"]
end
propose --> checkpoint --> apply --> cGate
cGate -->|зелёный| cS
cGate -->|зелёный| cC
cGate -->|"зелёный, есть свои темы"| cB
cS --> cT
cC --> cT
cB --> cT
```
**Глубины две, и они не про старательность, а про способ доказательства.**
**Сверка** — открыть дом темы, открыть дифф, сравнить. **Разбор** — построить
сценарий рассуждением, ничего не запуская.
**Третья глубина — доказательство** (прогнать, померить, построить путь) — в
цикле задачи не производится вовсе. Она требует машины и стоит часов, и потому
живёт в скилле `av-dev:code-deep-review`, который идёт по названной области и
время от времени. Проход, которому в плане назначили доказательство, получил план
не от конвейера задачи.
**Необратимое изменение состава не меняет — оно меняет адресата находки.**
Миграция схемы и данных, формат на диске, публичный контракт, имя, разошедшееся
по кодовой базе, — всё, что после мерджа не откатывается обратной правкой. Раньше
это был отрицательный тест метки `small`: такое изменение поднимало метку и
получало лишний проход. Поднимать больше нечего, и правило работает иначе:
находка по необратимому месту помечается `Действие: развилка` и уходит человеку,
а не чинится молча, каким бы мелким ни был дифф. Цена ошибки тут не в размере
правки, а в том, что её не отменить.
**Состав сверяется до коммита — по таблице тем выше.** Она и есть реестр: тема,
кто закрывает, против чего. Это единственная защита от промаха, который уже
случился: пропуск **не отличим от прохода без находок** (гейт зелёный, спеки
сошлись, отчёт выглядит полным), а заметить его мог бы только триаж, который сам
заполняется тем, что ему подали. Непущенное идёт строкой «не запускался» с
причиной, а не отсутствует. Цена молчащего пропуска измерена: семь находок и
отдельная задача на их дозакрытие.
**Сверять теперь дешевле, и это главный выигрыш от снятия метки.** Реестр был
переменным — он приезжал планом разметки и на каждой задаче выглядел иначе;
пропущенную тему приходилось искать сверкой двух списков. Реестр постоянный
сверяется взглядом: против каждой строки таблицы либо отчёт, либо названная
причина, по которой проход не пущен.
## Порядок прогона — граф, а не очередь
Таблица тем отвечает «что и против чего проверяется», порядок — «что кого
ждёт». Ступени остаются единицей **состава**, но порядок задают **не их номера**:
между ступенями 2 и 3 настоящих зависимостей нет — ни один проход не читает вывод
другого, — и очередь между ними была бы платой ни за что.
Рёбер два вида, и они разной природы. Путать их нельзя: первое про
**осмысленность** (на красном гейте проходу с мнением не о чем судить), второе
про **железо**.
| Ребро | Смысл | Между кем |
|---|---|---|
| **зависимость** | B не стартует, пока A не закончил, потому что без A задание B не определено | гейт → все проходы с мнением; все проходы → триаж |
| **конфликт за ресурс** | A и B не держат машину одновременно; кто из них первый — неважно, направления у ребра нет | проходы, помеченные «держит машину» |
**Узла, который считает состав, у графа нет.** Раньше первым узлом каждого
прогона стояла разметка и ребро «разметка → все» шло отсюда; состав постоянный, и
считать его больше нечем и незачем.
```mermaid
flowchart TD
autotests["autotests<br/>(ступень 1, держит машину)"]
specs["specs"]
code["code"]
basics["basics<br/>(только свои темы проекта)"]
triage["triage — единственный сток"]
autotests -->|зелёный| specs
autotests -->|зелёный| code
autotests -->|"зелёный, есть свои темы"| basics
specs --> triage
code --> triage
basics --> triage
```
Читается граф так: **всё, у чего входящие рёбра закрыты, уходит одним
сообщением**. Источник графа — гейт: он один по построению и идёт первым. После
зелёного гейта уходят разом `specs` и `code`, а с ними `basics`, если у проекта
есть свои темы; триаж стартует, когда вернулся последний. Глубина графа — три
шага при любой задаче, и это же его худший случай.
**Схема здесь старше прозы.** Она не иллюстрация к тексту, а сам алгоритм
планировщика; проза ниже объясняет рёбра и называет их цену. Разошлись — прав
граф, а расхождение чинится правкой текста.
**Ребро значит «A закончил раньше, чем B стартовал», и ничего больше.** В обычном
графе задач ребро тянет за собой данные — здесь нет, и это не деталь реализации.
Проход **не видит** находок других проходов, в каком бы порядке их ни запустили.
Вся ценность конвейера держится на разведённости: под всеми ролями одна модель с
одними априорными, и стоит показать ей чужой вывод — она согласится. Согласие
нескольких проходов и так не повышает `confidence` (см. «Честный предел»);
согласие **наведённое** ещё и маскируется под независимое подтверждение.
Единственный, кто получает чужие выводы, — триаж, и это его работа.
### Кто держит машину
Ресурс один и неделимый: **машина** — тесты, поднятый сервис, СУБД, порты, диск.
Проходы, заявившие его, сериализуются между собой на любой ступени; порядок
внутри цепочки произволен.
| Проход | Держит машину | Почему |
|---|---|---|
| `autotests` | да | запускает инструменты проекта — но он источник графа и один по построению |
| `triage` | да | проверяет оракул `major` запуском — но он сток и тоже один |
| `specs`, `code`, `basics` | нет | читают и рассуждают, ничего не исполняют |
**В цикле задачи цепочки за машину нет.** Оба прохода, что её держали —
`adversary` и `ops`, — переехали в скилл `av-dev:code-deep-review`; там правило
действует целиком, и дом его остаётся здесь. Оставшиеся двое машину держат, но
каждый один по построению: один источник графа, другой сток.
**Правило про ресурс, а не про имена.** Раньше здесь стояло именованное
исключение «`adversary` и `ops`»; оно рассыпается, как только проход начнёт
мерить или в проекте появится свой. Два прохода на одной машине соревнуются за
диск, CPU и за саму СУБД и выдают числа, которые не воспроизведутся, — а число,
снятое под конкурентную нагрузку, это находка с испорченным оракулом. Её
опровержение стоит дороже всего выигрыша от параллельности, и она хуже
отсутствующей: выглядит доказанной. Правило выведено из находок, целиком
державшихся на таких замерах; у каждого проекта они свои и лежат в журнале
`docs/review.md`.
Проект вправе пометить «держит машину» и другой проход — строкой в подразделе
**«Недоступно проверке»** файла `docs/review.md`: своего подраздела у пометки нет,
и заводить его канон не станет ради одного проекта. Читает её тот, кто строит
порядок прогона, то есть этот скилл. Снимать пометку с перечисленных нельзя.
### Находка «переделать форму» — прогон повторяется целиком
**Барьера стоимости в конвейере нет, и раннего выхода тоже.** Барьер существовал
ради независимой реализации — единственного прохода, чей счёт определялся объёмом
вывода, — и ушёл вместе с ней. Граф плоский, от гейта до триажа: защищать за
барьером нечего, а сериализация не бесплатна — она разводит по очереди то, что
могло идти разом.
Находка «**форму изменения** надо переделывать» ловится триажем, как и любая
другая; дальше правило одно. Находка чинится, и ревью кода запускается **заново с
гейта**, а не «доезжает» остатком по коду, которого через час не станет.
**Пересчитывать перед повтором нечего.** Состав постоянный, и второй прогон
идёт тем же составом, что первый; менять его нельзя даже «раз уж переделываем» —
конвейер, чей состав зависит от истории прогонов, не сверяется ни с чем.
Если прогон всё же остановлен на полпути, незапущенные проходы идут в границы
покрытия строкой «не запускался: прогон остановлен на <проход> из-за <находка>»,
поимённо, а **триаж на половине прогона не запускается**: его отчёт выглядит
полным, потому что агрегирует всё, что ему подали, — это тот же молчащий пропуск,
что и в разделе «Состав прогона».
Находка, которая чинится в пределах существующей формы (`Действие: инлайн`),
прогон не останавливает: дешевле дособрать все находки и починить пачкой, чем
гонять конвейер дважды.
### Линеаризация — когда графа мало
Граф можно вытянуть в одну цепочку. Это отступление, и оно называется в отчёте:
1. **сказал оператор** — «гони линейно». Набора называть не надо: линейный прогон
ничего не портит, он только дольше, и домысливать тут нечего;
2. **машина занята, и знает об этом вызывающий.** Рядом идёт другая задача,
поднят сервис, гоняется дорогая проверка проекта. Сам конвейер занятости
машины не видит — её обязан назвать тот, кто запускает;
3. **разбор самого конвейера** — когда выясняется, почему проход чего-то не
нашёл, порядок и изоляция важнее скорости.
Обратное отступление — **слить цепочку ресурса** (пустить меряющие проходы
разом) — бывает только по прямому слову оператора, и тогда в границы покрытия
идёт строка: какие проходы шли одновременно и что замеры этого прогона как
оракул слабее.
Режим объявляется в отчёте отдельной строкой: **`по графу`** — одним словом,
**`линейно`** — с причиной (какой именно из трёх).
## Прогон без change — сценарий обслуживания
Второй вызывающий конвейера — сценарий обслуживания скилла `av-dev:code-resolve`
(тулчейн и сборка, зависимости, гит-хуки, перенос, чистка). Он приходит **без
change**: у работы, не меняющей поведения, дельта-спек нет по построению.
**Копия.** Дом оси — `shared/axes.md` в репозитории плагина: режим делят конвейер,
сценарий обслуживания и два устава, и ни один из них им не владеет. Правится дом,
а не этот файл.
<!-- копия: режим-прогона из av-dev/shared/axes.md -->
**Прогон ревью идёт в одном из двух режимов, и режим — не глубина.**
- **По change** — обычный прогон цикла задачи: есть дифф и дельта-спеки, состав
постоянный и живёт в конвейере.
- **Без change** — дельта-спек нет по построению, и вместе с ними нет темы
`requirements`. План фиксирован и назван вызывающим; так идёт сценарий
обслуживания.
**Режим правит состав, а не глубину.** Глубина темы стоит в таблице тем
конвейера, одна на все прогоны по change; на прогоне без change её называет план
сценария — иначе проход, чьей темы в плане нет, взял бы глубину наугад.
**Третьего режима у конвейера нет.** Скилл `av-dev:code-deep-review` конвейер не
зовёт вовсе: состав, глубина и вход у него свои, а общее с конвейером — уставы
проходов и контракт находок.
<!-- /копия: режим-прогона -->
**План приходит вызовом и фиксирован сценарием**, а не выводится здесь. **Он же
называет темы и глубину каждого прохода** — таблица тем конвейера описывает
прогон по change, и тема `requirements` в ней есть, а здесь её предмета нет:
<!-- копия: план-обслуживания из av-dev/skills/code-resolve/references/maintain.md -->
| Тема | Дом | Кто закрывает | Глубина и вход | Когда |
| --- | --- | --- | --- | --- |
| `autotests` | `CLAUDE.md`, семантика гейта | `review-autotests` | как обычно: гейт запускается целиком | всегда |
| `operations` | `architecture.*`, раздел эксплуатации | `review-basics` | **сверка**: дом темы против диффа, потолок 2 | всегда |
| `conventions` + технический разбор | `conventions.*` | `review-code` | как в цикле: дом конвенций целиком, потолок 4 конвенционных, у техники потолка нет; сверка с инвариантами `CLAUDE.md` по темам `security`, `operations`, `architecture`, потолок 1 | дифф трогает код, а не только оснастку |
<!-- /копия: план-обслуживания -->
Триаж обязателен и здесь — он единственный сток и единственный, кто сверяет план
с исходом; на его вход подаётся этот план вместо таблицы тем. Тема
`requirements` в плане отсутствует за отсутствием предмета; `security` и
`architecture` закрыты сверкой с записанными инвариантами внутри `code` — ровно
так же, как в цикле задачи. Все три обязаны быть названы в границах покрытия.
Дом плана — сценарий, а не этот скилл: `av-dev:code-resolve`,
`references/maintain.md`, раздел «Ревью — план фиксирован сценарием».
**Правило гейта на таком прогоне работает жёстче обычного.** Правка, которая
трогает сам гейт, проверяется гейтом же — инструмент проверяет себя, — поэтому
сверяется не только цвет, но и состав шагов. Что считается составом, объявляет
проект семантикой гейта в `CLAUDE.md`; не объявил — это строка границ покрытия, а
не догадка прохода.
## Ступень 1 — Автотесты (обязательна)
Агент `review-autotests`, тема `autotests`. Гонит команду гейта из семантики
гейта в `CLAUDE.md` — либо засчитывает прогон, сделанный до ревью, — и
интерпретирует вывод.
**Тема и проход названы одинаково намеренно, а «гейт» осталось именем команды.**
Раньше тема звалась `autotests`, а проход — `gate`: одна сущность под двумя
именами, и вопрос проекта, адресованный одному имени, к другому не приезжал.
Слово «гейт» теперь значит ровно одно — барьер, который проект запускает; тема
шире него ровно на «чего в гейте намеренно нет».
**Гейт, прогнанный до ревью, второй раз не гоняется.** Задача приходит на ревью
с зелёным гейтом: сценарий решения доводит его до зелёного шагом `opsx:apply`,
сценарий обслуживания — своим шагом гейта. Повтор на неизменившемся дереве
вернёт тот же вывод, а стоит он минут — то есть платит ими ни за что.
**Признак один и проверяемый — отпечаток рабочего дерева.** Его снимают дважды:
тот, кто прогнал гейт, сразу после прогона, и проход перед началом работы.
<!-- дом: отпечаток-дерева -->
```sh
{ git rev-parse HEAD; git status --porcelain -uall; git diff HEAD;
git ls-files -o --exclude-standard -z | xargs -0 -r git hash-object; } | sha1sum
```
<!-- /дом: отпечаток-дерева -->
Сводку прошлого прогона, путь к логам шагов и отпечаток проход получает
**заданием** — их передаёт вызывающий скилл. Отпечатки совпали — проход читает
готовую сводку и логи, команду не запускает. Разошлись, отпечатка в задании нет,
логи недоступны — проход гонит гейт сам и ни у кого не спрашивает.
**Отказ здесь безопасен по построению.** Лишний прогон стоит минут, а
засчитанный чужой — красноты, которой никто не увидел. Временный каталог проекта
из отпечатка выпадает сам: `--exclude-standard` отбрасывает игнорируемое, а логи
шагов гейт пишет именно туда. У проекта, держащего временный каталог под git,
отпечатки не совпадут никогда — и он получит честный прогон вместо тихого
засчитывания.
**Переиспользуется команда, а не проход.** Тема `autotests` закрывается целиком:
логи проход читает сам, находки об отсутствующей верификации выдаёт как обычно.
Переиспользование он объявляет строкой сводки и строкой границ покрытия — чем
гейт прогнан, когда и на каком отпечатке. Молчащее переиспользование неотличимо
от собственного прогона, а разница между ними в том, кто видел вывод своими
глазами.
**Пока гейт красный — проходы с мнением не запускаются.** Оркестратор чинит и
перезапускает гейт. Исключение одно: отказ, унаследованный от базовой ветки
(гейт проверяет это прогоном на базе) — тогда он фиксируется находкой и не
блокирует.
Гейт возвращает не только «зелено/красно», но и находки класса **отсутствующая
верификация**: изменённые строки без покрытия, конкурентность без теста с
параллельным доступом, флаки-тест (не ниже `major`), недоступный инструмент.
Шаги выбираются по изменённым файлам: правка документации не гоняет тесты,
линтеры и детектор гонок. Пропуск при этом не молчит — он виден в сводке с
причиной и уезжает в границы покрытия, как и любой другой `SKIP`.
Шаги, которые красят гейт безусловно, перечислены в `CLAUDE.md` с причиной. Проходу
запрещено списывать такой отказ в мелочь.
## Ступень 2 — Сверка (обязательна)
Два прохода, оба против **записанного** критерия. Машину не держат ни один, ребра
между ними нет — уходят одним сообщением сразу после зелёного гейта, вместе со
ступенью 3, если она идёт.
- `review-specs` закрывает тему `requirements`. Критерий взят из **дельта-спек
предлагаемого изменения**, а не из proposal, сообщения коммита или описания
задачи. Сверка двунаправленная; направление `code → spec` важнее.
- `review-code` закрывает тему `conventions` **и делает технический разбор
кода** — это две его половины. Первая ищет дефект, который сработает сам, на
обычном входе: необработанная ветка отказа, пустое значение, граница диапазона,
перепутанный операнд, неосвобождённый ресурс, неверно применённый интерфейс
библиотеки. Вторая сверяет с конвенциями проекта, беря только ту их часть,
которая **не выражается правилом**: механизируемое уже проверила ступень 1.
**Третья его обязанность узкая и постоянная** — сверить дифф с записанными
инвариантами `CLAUDE.md` по темам `security`, `operations` и `architecture`.
Потолок 1 находка на все три темы разом: это не разбор темы, а объявленный
минимум, и в границах покрытия он называется именно так.
**Вход обоих постоянный и полный:** `specs` читает дельта-спеки и затронутые
актуальные спеки, `code` — дом конвенций целиком, до чтения диффа. Прежде вход
сужала метка `small` до дельта-спеки и индекса конвенций; узкий вход ловит
нарушение записанного рода и пропускает то, ради чего конвенцию писали абзацем,
— то есть экономил ровно на той работе, ради которой проход и зовут.
**Потолки у половин `code` раздельные, и это не бюрократия.** Конвенционных
находок больше по построению — родов навигации в разы больше, чем классов
технического дефекта, — и в общем списке они вытесняют техническую половину, чей
пропуск дороже. Раздельный потолок делает вытеснение невозможным: конвенционных
4, инвариантных 1, у технической половины потолка нет.
**Технический разбор — не тема, а обязанность прохода, и он единственный.**
Остальные читают код как материал для своей оптики: `specs` — против требований,
`basics` — против отказов окружения проекта. «Здесь
ошибка в логике» не говорит больше никто, и до недавнего времени не говорил
никто вовсе: `code` был проходом только по конвенциям, а дефект ловился разве что
случайно. Это была самая крупная дыра конвейера, и стоила она дороже любой
недосмотренной темы.
Recall темы `conventions` равен длине конвенций проекта — это предел любой
сверки, и снимает его не цикл задачи, а глубокое ревью области.
**Оба прохода на верхней модели, и по одной причине — цене пропуска.** У `specs`
это направление `code → spec`: надо заметить **отсутствие** — тихий фолбэк,
самодеятельный дефолт, проглоченную ошибку. У `code` это пропущенный дефект,
который поедет в прод. Ни то ни другое не оставляет следа ни в отчёте, ни в
границах покрытия; прочие проходы с мнением держат `opus` из-за цены **ложных**
находок, эти двое — из-за цены пропущенных.
**Эта ступень и есть цикл задачи.** С уходом тяжёлых проходов на ней держится всё,
что прогон вообще проверяет по существу: заказанное против сделанного, дефект,
который сработает сам, и конвенции проекта. Отсюда и решение не ставить потолка
технической половине.
## Ступень 3 — Темы проекта (только когда они есть)
Агент `review-basics`. Один проход, машину не держит, ничего не запускает и не
меряет — уходит одним сообщением вместе со ступенью 2, сразу после зелёного
гейта.
**Он приёмник проектных тем, и больше ничей.** Именных проходов конечное число, а
тем столько, сколько заведёт проект: без приёмника открытость списка тем была бы
обещанием без механизма. Темы **ядра** он больше не держит — риск и устройство
закрывает `code` сверкой с инвариантами, а разбор этих тем целиком уехал в
`av-dev:code-deep-review`.
**Запускается тогда и только тогда, когда ему есть что принимать.** Своих тем у
проекта нет — проход не идёт вовсе, и отчёт говорит об этом строкой: «свои темы
проекта не заведены, приёмник не запускался». Это единственное место, где состав
прогона зависит от проекта, и потому оно называется явно.
Глубина одна — **разбор**: построить сценарий рассуждением, дом темы против
диффа; потолок 4 находки. Прежде глубина приезжала планом разметки и на `small`
падала до сверки; плана нет, и падать ей больше неоткуда.
Чего он не делает ни на какой теме — замеров, эксперимента против драйвера,
построенного пути, карты проекта, границы домена. Всё это стоит машины или входа
шире диффа, то есть глубокого ревью области.
## Ступень 4 — Triage (обязательна)
Агент `review-triage`. **Единственный сток графа и единственный, кто агрегирует.**
Входящие рёбра — все запущенные проходы: пока хоть один не вернул отчёт, триаж не
стартует. Получает сырые выводы всех проходов, `git diff`, режим и **таблицу
тем**; возвращает финальный отчёт.
**На прогоне без change её место занимает план сценария** — см. «Прогон без
change»: сверять исход с планом триаж обязан и там, а другого перечня тем в том
прогоне не существует.
**Таблица тем на входе у триажа — не формальность, а сверка.** Он единственный,
кто видит и то, что заявлено, и то, что пришло: «тем шесть, отчёты покрывают
пять» — находка о самом прогоне, и заметить её больше некому. Раньше он получал
список запущенных проходов и потому мог сверить только состав; теперь сверяет
**темы**, а тема, оставшаяся без отчёта, — это то, чего список проходов никогда
не показывал. Таблица постоянная, и сверка потому дешевле прежней: сравнивать
приходится с одним и тем же реестром, а не с планом, который на каждой задаче
выглядел иначе.
Отсюда же правило, которое иначе выглядит придиркой: **триаж на неполном графе не
запускается**. Прогон, остановленный на полпути находкой «переделать форму», до
стока не доезжает — его отчёт агрегировал бы половину и выглядел бы полным.
Без триажа проходы дают порядка сорока замечаний при единицах существенных.
Потребитель здесь — оркестратор, который **молча реализует** всё, что прочитал:
цена нетриажированного отчёта — не потерянное время человека, а разросшийся от
вкусовщины код.
Порядок: дедупликация по причине → оракул для всего `critical`/`major`
понижение неподтверждённого до гипотезы → отсев вкусовщины → ранжирование по
ущербу × вероятности → потолок 7 пунктов в основном списке.
**Разметку действия ставит он же, и умолчание у неё одно — `инлайн`.** Развилку
получает только то, что инлайном чинить нельзя, и оснований у неё три: правка
меняет дельта-спеки, находка сидит в необратимом месте, находка трогает инвариант
`CLAUDE.md`. Остальное чинится молча — см. «Что происходит с находками дальше».
**Он же собирает строки «отложено в `av-dev:code-deep-review`».** Проход, упёршийся
в предел цикла — нужен замер, нужен прогнанный путь, нужен вход шире диффа, —
пишет об этом в своих границах покрытия; триаж сводит такие строки в одну секцию
отчёта. Без сведения они растворяются по отчётам проходов, и повод позвать
глубокое ревью не накапливается нигде.
## Контракт находок
Единый для всех проходов — [references/finding-contract.md](references/finding-contract.md).
Коротко: заголовок через **последствие**, обязательные поля `Файл`, `Severity`,
`Confidence`, `Оракул`, `Последствие`, `Предложение`, `Найдено проходом`.
`critical` без оракула или построенного пути не существует. Находка без поля
«Последствие» не выводится вовсе.
Каждый проход завершает вывод блоком `## Coverage of this pass`.
## Что происходит с находками дальше
**Умолчание одно, и оно называется прямо: находку чинит агент, молча.** Цикл
задачи устроен так, чтобы человек читал сводку, а не разбирал список замечаний;
всё, что чинится в пределах одобренной формы решения, помечается `Действие:
инлайн`, уходит агенту дословно вместе с оракулом и логированию не подлежит.
Прогон, вернувший человеку десяток вопросов, свою работу не сделал.
Из умолчания два выхода, и оба узкие:
- **`Действие: развилка`** — вопросом с вариантами и ценой каждого туда, где
проект держит вопросы (это знает вызвавший скилл, а не конвейер ревью).
Помечается так **только** то, что инлайном чинить нельзя, и оснований ровно
три: находка по **необратимому** месту (миграция, формат на диске, публичный
контракт), находка, трогающая **инвариант** `CLAUDE.md`, и находка, чья правка
меняет **дельта-спеки** — то есть отменяет одобренное человеком.
По первым двум основаниям оркестратор **не останавливается**: он урезает
изменение до остатка и доводит его. Третье старше: правка, меняющая
дельта-спеки, отменяет одобрение, и оркестратор **возвращается на чекпоинт**
(`av-dev:code-resolve`, `references/solve.md`, шаг 5). Вопрос в запись при этом
остаётся, но возврата не заменяет — иначе одобренный дизайн переделывался бы
молча.
- **урожай** — находка реальная, но не для этого мерджа: отложенный `major`,
развилка, решённая «потом», пачка `nit`. Конвейер отдаёт её **списком** в
отчёте: формулировка, оракул, откуда взялась (какой проход, какой change).
**Задачи из урожая заводятся только по слову человека, и это правило, а не
вежливость.** Спрашивает не конвейер: список уезжает вызывающему и показывается
человеку **одной репликой на весь хвост задачи** — вместе с тем новым, что
предлагает записать синк документации (`av-dev:code-resolve`,
`references/solve.md`, шаг 6). Два вопроса про одно и то же — «что из найденного
переживёт задачу» — стоили бы двух переключений вместо одного. Сказал «заводим» —
зовётся `av-dev:task-track`, у него на этот вход отдельный сценарий «задачи из ревью и
аудита»: свой формат, кластеризация по причине, дедуп против беклога и кладбища.
Не сказал — урожай остаётся строками доклада, и это исход, а не потеря. Прогон,
заводящий задачи сам, наполняет беклог работой, которую никто не выбирал; на
проекте, где очередь работ ведёт один человек, это и есть главная цена лишней
находки. Каталога задач в проекте нет — урожай остаётся списком тем более, и
это говорится строкой.
Остальное не меняется:
- `Promote candidates` — по процедуре [references/promote.md](references/promote.md):
находка → конвенция → правило линтера → **удаление формулировки из конвенций**.
Третий шаг обязателен. **Сама конвенция заводится по слову человека** — той же
репликой, что и задачи из урожая: её строка станет входом каждого следующего
прогона, и из всего, что пишет хвост задачи, она связывает дальше всего.
- Дефект, проскочивший ревью и всплывший позже, идёт в журнал проекта
([references/review-journal.md](references/review-journal.md)) — сразу, не
ретроспективно: теряется именно то, почему дефект не поймали.
- **Отчёт триажа сохраняется вместе с изменением**`openspec/changes/<id>/review/`.
Он единственное, по чему потом видно, что было найдено и что из этого не
заведено: нулевой урожай при непустом отчёте виден сразу.
**Вместе с изменением он и переезжает:** после `opsx:archive` его адрес —
`openspec/changes/archive/<id>/review/`. Кто ищет отчёт после архивации
(приёмщик на груминге `av-dev:task-groom`, разбор дефекта), смотрит **оба**
пути; «отчёта нет» объявляется, только когда пуст и архивный, иначе самый
дорогой сценарий «состав ревью неизвестен, гоняем заново» срабатывает на
каждой доведённой задаче.
## Честный предел
Модель воспроизводит медиану публичного кода, смещённую к популярному и
туториальному: отсюда тяга к интерфейсам ради интерфейсов, лишним мокам и
конфигурируемости, которую никто не просил. **«Идиоматично» и «распространено» —
разные вещи**; проходы обязаны различать их и опираться на поимённое положение
руководства, а не на ощущение частотности.
Согласие нескольких проходов — **не подтверждение**: это один источник,
высказавшийся несколько раз. Совпадение повышает приоритет, но не `confidence`.
Что недоступно **этому** проекту принципиально — перечисляет «Недоступно
проверке» в `docs/review.*`, по темам, и оба его подраздела целиком уезжают в
границы покрытия. **Тема, у которой нет дома, — тоже граница покрытия**, и она
объявляется на каждом прогоне, а не разово.
Независимо от проекта недоступно:
- поведение внешних систем в их будущих версиях;
- реальный профиль нагрузки и то, что на самом деле лежит в данных;
- завязка внешних потребителей на текущую форму ответа;
- суждение «этой функциональности не должно существовать».
Отдельно и честно: **поимённая сверка с положениями руководств по стилю языка не
задаётся ни одним проходом.** Проход про идиоматичность упразднён, его способные
части переселены (эксперимент против поведения библиотеки и драйвера — в `ops`,
«не изобретаем ли то, что уже есть в библиотеке» — в `architecture`, и оба теперь
живут в `av-dev:code-deep-review`), но различение «идиоматично против
распространено» не спрашивает никто. Класс обратимый — портит форму кода, не
данные, — и его надо признавать в границах покрытия, а не считать проверенным.
**Форму решения в цикле не судит никто, и это сознательное сужение.** Ни ревью
дизайна до кода, ни архитектурного прохода после — обоих сняли, и оба ушли по
одной причине: суждение о форме стоит разговора с человеком, а разговор внутри
задачи растягивает её в часы. Форму одобряет **человек на чекпоинте**, до кода, и
это единственное место цикла, где решение о ней принимается. Всё, что видно
только по написанному коду — второй способ делать уже делаемое, лишний слой,
интерфейс ради мока, — ловится глубоким ревью области, то есть позже и не всегда.
Класс идёт строкой в границы покрытия каждого прогона.
**Запуском в цикле не проверяется ничего сверх гейта, и это на всякой задаче.**
Формулировка «не запускается ничего» была бы короче и была бы ложью: гейт
запускает инструменты проекта, а триаж проверяет оракул `critical`/`major`
запуском — оба идут всегда. Не проверяется **проходом с мнением**: построенный
путь атаки (его надо прогнать), поведение библиотеки и драйвера в вырожденном
случае (достаётся только экспериментом), любое число — время удержания
блокировки, пик кучи, темп роста журнала, стоимость на годовой истории.
**Три темы риска и устройства смотрятся только против записанных инвариантов.**
Отдельная строка, и она обязательна в каждом отчёте: `security`, `operations` и
`architecture` закрывает `code` сверкой с `CLAUDE.md`, потолком 1 находка на все
три. Свойства, которого нет в инвариантах, в цикле не спросит никто. Это не
«глубина ниже» — это **другой дом темы**, куда более узкий, и путать одно с
другим нельзя.
**Ось времени в цикле не смотрит никто.** Обратима ли миграция, что станет с
записями новой версии после отката, как узел ведёт себя через неделю роста —
раньше эти вопросы задавал приёмник тем на метке `medium`, теперь метки нет, а
приёмник держит только свои темы проекта. Взамен работает адресация: находка по
необратимому месту идёт человеку развилкой, а не чинится молча. **Это не
равноценная замена, и подменять одно другим нельзя:** развилка срабатывает,
только если находку кто-то сделал, а по оси времени в цикле её теперь делает
разве что инвариант.
**Решения и измеренные числа проекта прогон не читает вовсе.** `adr.*` и
`research.*` — процессные документы. Отсюда две строки в границы покрытия каждого
прогона: расхождение изменения с записанным решением ловится не здесь, а сверкой
документации; число, на которое опирается находка, обязано быть снято **на этом
прогоне**, иначе находка не поднимается выше гипотезы. Раньше числа брались из
`docs/research/`, и находка выглядела доказанной чужим замером неизвестной
свежести.
**Темы при этом названы все — но закрыты они по-разному, и это надо читать
буквально.** «Тема `security`, глубина сверка» не значит «безопасность
проверена»: значит, что дом темы открыли, дифф посмотрели и сравнили.
**Доказательства в цикле задачи нет, и это самая крупная его граница.** Класс
дефектов, который виден только построенным путём и снятым числом — гонка,
деградация под нагрузкой, исчерпание ресурса, откат бинаря поверх новой схемы, —
здесь не ловится ничем.
Это сознательная сделка, а не пробел в устройстве: тяжёлые проходы оплачивались
на каждой задаче, где запускались, а получались на немногих. Теперь они живут в
`av-dev:code-deep-review` и оплачиваются тогда, когда их решают получить.
Проверяется сделка не рассуждением, а двумя следами: **строками «отложено»** в
отчётах — если по одному месту повторяется один и тот же неснятый замер, глубокий
прогон просрочен, — и **журналом дефектов**: класс, всплывающий после мерджа,
значит, что прогон надо звать чаще.
Так же честно и про упразднённый проход: **«не знаю, чего не знаю» больше
не достаёт никто.** Проход независимой реализации писал свою версию узла, не
открывая существующую, и диффил по решениям — декомпозиция, владение данными,
модель конкурентности, форма решения там, где спека выбора не сделала. Он снят по
решению оператора о **стоимости** — счёт определялся объёмом вывода, и на прогон
он тратил больше всех остальных проходов вместе, — а не по замеру, который
[calibration.md](references/calibration.md) требует перед удалением. Значит и
записывается это как сознательное сужение, а не как «класс оказался пустым».
Класс идёт строкой в границы покрытия каждого прогона — там же, где проект
перечисляет своё в подразделе «перестали проверять сознательно».
Это и есть причина, по которой конвейер готовит ревью, а не заменяет его.
## Ссылки
- [references/project-facts.md](references/project-facts.md) — что нужно проходу
и где это лежит в документах проекта; таблица поразрядной деградации.
- Skill `av-dev:code-deep-review` — глубокое ревью области кода: там живут
`review-adversary`, `review-ops` и `review-architecture`, там же единственное
место процесса, где находка доказывается прогоном и замером, а форма решения
вообще обсуждается.
- Skill `av-dev:canon` — приведение проекта к канону документов.
- [references/finding-contract.md](references/finding-contract.md) — контракт находок.
- [references/promote.md](references/promote.md) — промоут находка → конвенция → правило → удаление.
- [references/calibration.md](references/calibration.md) — калибровка инъекцией, вердикты keep/retune/drop.
- [references/review-journal.md](references/review-journal.md) — журнал проскочивших дефектов.
@@ -27,7 +27,7 @@
```mermaid
stateDiagram-v2
state "проход в профиле" as live
state "проход в составе прогона" as live
state "retune №1 — правка charter'а" as r1
state "retune №2 — последняя попытка" as r2
state "проход удалён" as dead
@@ -55,7 +55,7 @@ stateDiagram-v2
поведения библиотеки и драйвера**, — и находка, воспроизведённая числом, отменила
решение, принятое по ощущению.
## Состав проходов принадлежит плагину, а не проекту
## Состав проходов принадлежит скиллу, а не проекту
Проходы общие. Проект не может удалить проход — он может **не звать** его, и
тогда это идёт строкой «не запускался» в границы покрытия, как любой другой
@@ -67,7 +67,7 @@ stateDiagram-v2
- **правка charter'а — правка для всех проектов.** Прежде чем сужать
формулировку под свою боль, проверь, не место ли ей в документах проекта: предмет проверки
живёт там, метод — в charter'е;
- **удаление прохода из плагина требует замера на двух проектах**, а не на одном:
- **удаление прохода из конвейера требует замера на двух проектах**, а не на одном:
класс, не всплывший здесь, мог быть единственным работающим там.
## Пробы дефектов по проходам
@@ -79,14 +79,17 @@ stateDiagram-v2
| Проход | Класс дефекта для инъекции | Заготовка пробы |
|---|---|---|
| `review-gate` | отсутствующая верификация | убрать тест на изменённую ветку, оставить код рабочим |
| `review-autotests` | отсутствующая верификация | убрать тест на изменённую ветку, оставить код рабочим |
| `review-specs` | поведение вне спеки | добавить незаказанный фолбэк-дефолт на пустом входе |
| `review-code` | нарушение прозаической конвенции | увести штатный отказ мимо единой точки трансляции ошибки |
| `review-code` | технический дефект | не проверить возвращённую ошибку в ветке раннего возврата |
| `review-rubric` | нарушенное свойство узла | у клиента внешнего сервиса убрать таймаут и протяжку `context` |
| `review-reimpl` | форма решения | размазать решение по трём слоям там, где хватало одной функции |
| `review-basics` | отказ, видимый чтением | убрать обработку ошибки записи так, чтобы отказ считался успехом |
| `review-basics` | своя тема проекта | нарушить правило из документа, у которого нет именного прохода |
| `review-architecture` | второй способ | завести вторую точку генерации id мимо единой |
| `review-code` | инвариант проекта | нарушить записанный в `CLAUDE.md` запрет по темам `security`, `operations` или `architecture` |
| `review-adversary` | построенный путь | принять внешний идентификатор без разбора до запроса в хранилище |
| `review-ops` | деградация окружения | убрать обработку недоступности внешней зависимости в фоновом цикле |
| `review-ops` | ось времени | убрать обработку недоступности внешней зависимости в фоновом цикле |
| `review-triage` | шум | подать 20 находок, из них 15 вкусовщина и 3 дубля — проверить потолок и дедуп |
Метрик сверх этого не заводим. Precision, корреляция между проходами, стоимость
@@ -98,7 +101,7 @@ stateDiagram-v2
## Когда калибровать
- при заведении нового прохода — **до** включения в профиль по умолчанию;
- при заведении нового прохода — **до** включения в состав прогона;
- при правке charter'а существующего — иначе непонятно, правка помогла или нет;
- при появлении записи в журнале проскочивших дефектов — калибруем тот проход,
который должен был поймать;
@@ -10,10 +10,10 @@
- Файл: internal/<пакет>/<файл>.go:120-134
- Severity: critical | major | minor | nit
- Confidence: high | medium | low
- Оракул: <падающий тест / команда с выводом / положение гайда / нет>
- Оракул: <падающий тест / команда с выводом / положение руководства / нет>
- Последствие: <что произойдёт и при каких условиях>
- Предложение: <конкретное изменение>
- Найдено проходом: <имя агента>
- Найдено проходом: <имя агента; у проходов с раздельными потолками — имя и половина, например `code/техника`>
```
## Правила
@@ -24,7 +24,7 @@
Симптом в заголовке — это заявка на то, что читатель сам достроит последствие;
он не достроит, он просто починит симптом.
- **`critical` без оракула или построенного пути не существует.** Оракул — это
падающий тест, вывод выполненной команды или поимённое положение гайда. Не
падающий тест, вывод выполненной команды или поимённое положение руководства. Не
«вероятно, здесь гонка», а прогон детектора гонок с его выводом.
- **`confidence: low` — это «так обычно пишут».** Такие находки допустимы, но не
поднимаются выше `minor`. Частотность конструкции в публичном коде — не
@@ -38,11 +38,13 @@
- **`critical` по основанию «нарушен инвариант проекта» требует инвариантов.**
Ссылка идёт на пункт раздела инвариантов `CLAUDE.md` дословно. Без них основание
недоступно — см. [project-facts.md](project-facts.md), поразрядная деградация.
- **Расхождение — не дефект, пока не названо последствие.** Особенно для прохода
независимой реализации: «я бы сделал иначе» без последствия не выводится.
- **Расхождение — не дефект, пока не названо последствие.** Особенно для
архитектурного прохода: «я бы сделал иначе» без последствия не выводится.
## Шкала severity
Severity — ось процесса; перечень осей — [shared/axes.md](../../../shared/axes.md).
| Severity | Что это | Пример |
|---|---|---|
| `critical` | нарушение инварианта проекта, потеря или порча данных, утечка секрета, построенный путь к отказу | запись потеряна при слиянии; тело пользовательской выгрузки в поле лога |
@@ -73,13 +75,23 @@
1. `Блокирует мердж` (≤3, каждая с оракулом);
2. `Стоит исправить сейчас` (≤4);
3. `Гипотезы без доказательства` — что понижено и почему;
4. `Promote candidates` — кандидаты в конвенцию или правило линтера;
5. `Границы покрытия` — сводная, обязательная.
4. `Урожай` — реальные находки не для этого мерджа: формулировка, оракул,
происхождение. Задачи из них заводит человек своим словом, не отчёт;
5. `Отложено в av-dev:code-deep-review` — что доказывается только запуском,
замером или входом шире диффа: тема, место, чем проверяется;
6. `Promote candidates` — кандидаты в конвенцию или правило линтера;
7. `Границы покрытия` — сводная, обязательная.
Перед секциями — сводка для человека: профиль и режим прогона, состояние гейта,
**перечень запущенных проходов поимённо с исходом каждого**, сколько находок
пришло на вход и сколько осталось. Перечень обязателен: пропуск прохода не
отличим от прохода без находок, и назвать его больше некому.
Перед секциями — сводка для человека: режим прогона, состояние гейта, **перечень
тем с исходом по каждой**, сколько находок пришло на вход и сколько осталось,
сколько из них помечено `инлайн` и сколько `развилка`.
**Реестр сводки — темы, а не проходы, и это не оформление.** Перечень запущенных
проходов отвечает «все, кто должен был, отработали» и молчит о том, что именно
осталось непроверенным: уехавший в другой скилл проход уносит тему с собой
беззвучно. Перечень тем называет тему, её дом, глубину и исполнителя — и тема,
оставшаяся без отчёта, видна сразу. Перечень проходов из сводки не исчезает, но
идёт **внутри** него, колонкой «кто закрывает».
Каждая находка в секциях 1–2 несёт дополнительное поле:
@@ -87,10 +99,12 @@
- Действие: инлайн | развилка
```
`инлайн` — оркестратор чинит сам, не спрашивая и не логируя. `развилка` — цена
исправления сопоставима с переработкой, либо выбор меняет scope, либо решение
трогает инвариант: уезжает вопросом с вариантами и ценой каждого туда, где
проект держит вопросы, а работа продолжается на остатке.
`инлайн` — оркестратор чинит сам, не спрашивая и не логируя, **и это
умолчание**. `развилка` — узкий выход с тремя основаниями: правка меняет
дельта-спеки, находка сидит в необратимом месте (миграция, формат на диске,
публичный контракт), находка трогает инвариант. Она уезжает вопросом с вариантами
и ценой каждого туда, где проект держит вопросы, а работа продолжается на
остатке.
Потребитель отчёта — оркестратор, который **реализует прочитанное**. Поэтому
потолок в 7 пунктов — не забота о внимании читателя, а защита кодовой базы от
@@ -0,0 +1,141 @@
# Откуда проход берёт проектную конкретику
Конвейер общий, находки — проектные. Проход, не знающий, что в этом проекте
нельзя нарушать, чем краснеет гейт и сколько данных реально идёт через узел,
выдаёт правдоподобные общие места: их дорого опровергать и нечем подтверждать.
Отдельного файла-брифа **нет**. Проектная конкретика живёт в документах канона,
и проход читает их напрямую: пути жёсткие, посредник не нужен, а
второй дом для тех же фактов разошёлся бы и выглядел актуальным.
Определение канона держит скилл `av-dev:canon`. Здесь только карта «тема →
её дом → что оттуда берётся».
## Карта тем
**Дом бывает файлом или каталогом** — `docs/security.md` и `docs/security/`
называют одну и ту же тему. Форму дома называет задание прохода; проход её не
угадывает.
| Тема | Дом | Что оттуда берётся |
| --- | --- | --- |
| `requirements` | `openspec/specs/`, `openspec/changes/<id>/specs/` | нормативное поведение и дельты изменения |
| `autotests` | `CLAUDE.md`, семантика гейта | команда гейта, чем краснеет безусловно, чего в нём нет, кто гоняет дорогое |
| `conventions` | `docs/conventions.*` | конвенции прозой и **что уже механизировано** правилом |
| `architecture` | `docs/architecture.*` | компоненты и capability, единые точки проекта |
| | источник `docs/passport.*` | что система делает и **чего не делает**, граница домена |
| `security` | `docs/security.*` | периметр, недоверенный вход, из чего строятся пути и ключи, что вне модели |
| `operations` | `docs/architecture.*`, раздел эксплуатации | окружение, внешние зависимости поимённо, наблюдатель, характер потока |
| | источник `docs/database.*` | чем физически лежит запись, что при чтении и записи, настройки с числовым значением |
| *тема проекта* | её **свой** документ в `docs/` | то, что проект счёл нужным записать |
**`docs/adr.*` и `docs/research.*` в этой карте нет намеренно.** Они процессные
документы: прогон ревью их не открывает. Раньше первый питал тему `architecture`,
второй — `operations` и `requirements`; обе строки убраны, и цена этого названа в
`SKILL.md`, раздел «Честный предел».
**Дом темы зависит от того, кто её закрывает.** В цикле задачи темы `security`,
`operations` и `architecture` смотрятся не против домов из этой таблицы, а против
**инвариантов `CLAUDE.md`**, и закрывает их `code`. Полные дома открывает скилл
`av-dev:code-deep-review` своими проходами. Таблица описывает полный дом темы;
что из него открыто на этом прогоне, говорит состав прогона.
Сквозное, не привязанное к теме:
| Что нужно проходу | Где лежит |
| --- | --- |
| инварианты **с severity рядом с формулировкой** | `CLAUDE.md``AGENTS.md`, если он рядом), раздел инвариантов |
| что запускать запрещено, с путями; `testdata`; куда писать временное; имя основной ветки | `CLAUDE.md` |
| типовые узлы, типовые ложноположительные, **вопросы по темам**, недоступно проверке | `docs/review.*`, раздел настройки |
| прецеденты: воспроизведённые дефекты с оракулом | `docs/review.*`, журнал |
**Вопросы проекта привязаны к теме, а не к имени прохода.** Раньше блок в
`docs/review.md` адресовался поимённо (`ops: <вопрос>`), и когда проход уехал в
другой скилл, вопрос перестал задаваться молча. Тема переезд прохода
переживает.
## Сшивать обязаны проходы
Раньше эти факты лежали рядом в одном файле, и соседство работало само. Теперь
они разложены по домам, и **проход обязан собрать их сам** — иначе снимет верное
число и честно понизит находку до гипотезы, потому что сравнить будет не с чем.
Два обязательных стыка:
- **замер + настройка.** «Пик 768 МиБ» — аномалия только рядом со строкой
«запись лежит сжатой и распаковывается целиком»; «блокировка удерживалась
5.019 с» — гарантированный отказ соседа только рядом с известным таймаутом
занятости. **Число проход снимает сам, на этом прогоне**, настройки берёт из
`docs/database.md`, и сшивает их `ops` в глубоком ревью — в цикле задачи не
снимает чисел никто. Раньше числа брались из
`docs/research/`; теперь этот документ процессный, и замер неизвестной свежести
больше не выдаёт себя за оракул.
- **инвариант + обратимость.** severity берётся из `CLAUDE.md`; если её там
нет — она **выводится по обратимости последствия** и помечается «выведена по
обратимости», а не выдаётся за решение проекта.
**У `basics` стыков нет, и это не упущение.** Он не меряет, поэтому сшивать число
с настройкой ему нечего; единственное его основание для `critical` — инвариант из
`CLAUDE.md`, всё остальное он формулирует условиями и оставляет гипотезой. Его
вход намеренно узкий: дома тем из задания плюс инварианты и журнал. Широкий вход
есть только у `architecture`, а он работает в глубоком ревью. Греп по базе ему разрешён
точечный — «есть ли второй вызывающий», — но обход всей базы и инвентарь
концепций не его работа.
**Дома передаются адресом, а не пересказом, и это правило пережило проход,
который его исполнял.** Прежде темы раздавал `review-scope`: он находил дома и
называл их путём с разделом, ничего не пересказывая. Прохода нет, состав
постоянный, но правило то же — проход, получивший проинтерпретированный периметр,
не заметит, что интерпретация неверна.
## Деградация — поразрядная
Документа нет — деградирует то, что из него читалось, и **только оно**. Каждый
проход пишет **свою** строку в границы покрытия; триаж собирает их в один
список и **не сливает в одну строку**: разные пробелы чинятся разным — периметр
пишется руками за десять минут, а числа требуют замера.
**Кто какой документ читает — из документа не выводится, а назначается планом.**
Документ питает тему (это записано на стороне канона, таблица «Роли документов и
темы ревью»), а тему на этом прогоне закрывает тот, кто назван в составе прогона; вся
раскладка «тема → кто закрывает → против чего» — в `SKILL.md` этого скилла и
больше нигде.
**Списка читателей не ведёт никто, и это не пробел.** Он жил бы на стороне
канона, а документ живёт дольше, чем раскладка проходов: список разошёлся бы с
конвейером молча и при этом выглядел актуальным. Однажды уже разошёлся.
Ниже — только **последствие** отсутствия дома, и оно называет самое дорогое, а не
всех пострадавших.
| Нет дома | Что деградирует |
| --- | --- |
| `CLAUDE.md` без инвариантов | `critical` по основанию «нарушен инвариант проекта» не присваивается никем |
| `docs/security.*` | тема `security` остаётся без дома: вопросы задаются по коду, `critical` не ставится, периметр неизвестен |
| `docs/database.*` | замер не с чем сравнить: находка темы `operations` не поднимается выше гипотезы |
| `docs/passport.*` | тема `architecture` теряет границу домена и вырождается в общее мнение |
| `docs/review.*` | `triage` отсеивает вслепую: типовых ложноположительных нет; вопросы проекта по темам не задаются |
| `docs/conventions.*` | вторая половина `code` идёт вхолостую: записанных конвенций нет |
| `docs/architecture.*` | «не появился ли второй способ» не проверяется — единых точек не знает никто; тема `operations` теряет перечень внешних зависимостей |
Строка в границах покрытия обязана называть **причину**: «`docs/security.md` в
проекте нет» читается иначе, чем «есть, но периметр не назван». Без причины
строка неотличима от «мы просто не стали» и перестаёт читаться на третьей задаче.
**Документов канона нет вовсе** — проект не приведён к канону. Это не повод
работать вслепую: скажи об этом строкой и предложи `av-dev:canon`. Одна
операция на проект против деградации на каждой задаче.
## Правило чтения
- **Читай в источнике, не по памяти.** Документы правятся по ходу работы, в том
числе этой же задачей.
- **Число без происхождения — условие, а не утверждение.** Число, чей источник по
ссылке не подтвердился, читается как условие и **называется расходящимся**, а
не подменяется догадкой.
- **Пустое, названное пустым, — это факт.** «Внешних зависимостей нет — смотри
на диск и на СУБД» экономит обязательный вопрос. Отсутствие строки — не факт,
а пробел, и его надо назвать в границах покрытия.
- **Свойство, ставшее правилом линтера, из конвенций удалено** и лежит в
перечне механизированного — в `docs/conventions/README.md`, если конвенции
каталогом, и отдельным разделом `docs/conventions.md`, если файлом. Проверять
его проходом — тратить внимание на уже проверенное.
@@ -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
@@ -52,6 +52,13 @@ flowchart TD
тема относится к поведению системы, а не к тому, как мы пишем код, — это не
конвенция, а требование: заводится дельта-спека обычным путём.
**Конвенция заводится по слову человека, и это не формальность.** Одна её строка
становится входом каждого следующего прогона ревью и критерием для всех будущих
задач — из всего, что пишет хвост задачи, конвенция связывает дальше всего.
В цикле задачи она поэтому **предлагается**, а не заводится: строка предложения
называет проверяемое свойство и проход, который его нашёл, и по этой паре человек
решает (`av-dev:code-resolve`, `references/solve.md`, шаг 6, такт второй).
Промоут идёт **тем же путём, что change → spec**: правка попадает в тот же
коммит, что и исправление кода, с пометкой в сообщении — история промоутов
остаётся видна в `git log` по файлу конвенций.
@@ -75,6 +82,12 @@ flowchart TD
хук блокирует любой коммит, и правило снимут первым же раздражённым движением.
Приводить код в соответствие — часть шага 2, отдельным коммитом.
**Отсюда и место шага 2: он не помещается в хвост чужой задачи.** Конфиг,
сканер и приведение кода к зелёному — это работа размером с задачу, и сделанная
попутно она удваивает прогон, который человек заводил ради другого. Согласованный
промоут даёт **строку конвенции сейчас** и **задачу `chore` на механизацию**;
задачу заводит `av-dev:task-track` тем же словом, что и саму конвенцию.
## Шаг 3. Удаление из конвенций и из промптов
**Шаг, который пропускают чаще всего, и единственный, ради которого затевались
@@ -85,9 +98,10 @@ flowchart TD
- из файла конвенций убирается формулировка правила; остаётся, если нужно, одна
строка «проверяется линтером `<имя>`» — но только там, где без неё раздел
теряет связность;
- правило переезжает в **перечень механизированного в
`docs/conventions/README.md`** — со ссылкой на место механизации: конфиг
линтера, собственный анализатор, тест-сканер исходников. Непойманное место
- правило переезжает в **перечень механизированного в доме конвенций**
(`docs/conventions/README.md` у каталога, отдельный раздел
`docs/conventions.md` у файла) — со ссылкой на место механизации: конфиг
линтера, собственный анализатор, тест-сканер исходников. Не названное место
означает, что проход будет добросовестно проверять уже проверенное;
- из контекста инструмента спек убирается дубль, если он там был.
@@ -100,8 +114,8 @@ Charter'ы проходов при этом **не правятся**: они о
принципиально не выражается правилом.** Файл конвенций на несколько сотен строк
размазывает внимание модели по тривиальному — она добросовестно проверит
именование полей лога и не дойдёт до формы решения. Каждая строка конвенций,
которую можно было бы проверить машиной, оплачивается непойманным дефектом
где-то ещё.
которую можно было бы проверить машиной, оплачивается дефектом, который не
поймали где-то ещё.
## Обратное движение
@@ -1,23 +1,23 @@
# Журнал дефектов
Артефакт проекта, а не плагина: файл живёт в репозитории — **`docs/review.md`**,
слот канона `av-dev-pm`. Здесь описано, зачем он и какой формы, потому что без
него конвейер не учится: находки закрываются, причины непоймания теряются, и один
и тот же класс проскакивает второй раз.
слот канона документов. Здесь описано, зачем он и какой формы, потому что без
него конвейер не учится: находки закрываются, а почему их не поймали — забывается,
и один и тот же класс проскакивает второй раз.
Тот же файл держит **настройку конвейера под проект** — типовые узлы, типовые
ложноположительные, вопросы к проходам, недоступно проверке. Это не соседство по
ложноположительные, вопросы по темам, недоступно проверке. Это не соседство по
случаю: все четыре раздела — производные калибровки, а журнал им источник.
## Что туда попадает
**Воспроизведённый дефект — с пометкой `проскочил` или `пойман ревью`.**
Записывается **сразу**, а не ретроспективно: со временем теряется не сам факт, а
причина непоймания — единственное, ради чего журнал существует.
Записывается **сразу**, а не ретроспективно: со временем теряется не сам факт, а то,
почему дефект не поймали, — единственное, ради чего журнал существует.
Пометка делит журнал на две выборки с разным назначением:
- **проскочил**эвал-сет для калибровки конвейера. Реальный промах сильнее
- **проскочил**проверочный набор для калибровки конвейера. Реальный промах сильнее
синтетической пробы: синтетические смещены в сторону тех, которые уже умеешь
придумывать;
- **пойман ревью** — прецеденты с оракулом. Самая сильная опора, какая у прохода
@@ -29,7 +29,7 @@
и `docs/adr/`.
Отдельно сюда попадают **решения о составе прогонов**: перестали звать проход,
понизили профиль правилом, сузили класс проверяемого. Не потому, что это промах,
переселили его в другой скилл, сузили класс проверяемого. Не потому, что это промах,
а потому, что здесь лежит цена: если что-то теперь проскочит, первый вопрос —
«не тот ли это класс, который мы перестали проверять».
@@ -41,8 +41,7 @@
## Форма записи
**Это дом формы, и у него есть копия.** Скелет `docs/review.md`, который кладёт
в проект `av-dev-pm` (`skills/canon/references/skeletons.md`), повторяет её
дословно — он уезжает в репозиторий и обязан там что-то говорить. Правка формы
в проект `av-dev:canon`, повторяет её дословно — он уезжает в репозиторий и обязан там что-то говорить. Правка формы
здесь **обязана** тянуть правку скелета и запись в журнал версий канона; иначе
проекты продолжат писать по старой форме, а конвейер — ждать поля, которого нет.
Дословность сверяет `scripts/copies.py` маркетплейса по маркерам ниже — но
@@ -77,12 +76,13 @@
Три адреса, и выбор между ними — половина ценности журнала:
- **в документ проекта** — если проход не мог знать факта. Адрес зависит от рода
факта, и карта их всех — [project-facts.md](project-facts.md): объём и
измеренное число → `docs/research/`; настройка хранилища → `docs/database.md`;
факта, и карта их всех — [project-facts.md](project-facts.md):
настройка хранилища → `docs/database.md`;
что необратимо и какой шаг гейта красит безусловно → `CLAUDE.md`; периметр и
недоверенный вход → `docs/security.md`. **Вопрос конкретному проходу**, если
промах лечится не фактом, а заданным вопросом, → раздел «Вопросы к проходам»
того же `docs/review.md`. Самый частый адрес и самый дешёвый. Прежде чем
недоверенный вход → `docs/security.*`. **Вопрос по теме**, если промах лечится
не фактом, а заданным вопросом, → раздел «Вопросы по темам» того же
`docs/review.*`; адресуй теме, а не имени прохода — проход уедет в другой
скилл, тема останется. Самый частый адрес и самый дешёвый. Прежде чем
править charter, проверь, не хватит ли факта или вопроса: charter общий для
всех проектов, документ — про этот.
- **в конвенции или в правило линтера** — если свойство выражается
+189
View File
@@ -0,0 +1,189 @@
---
name: doc-healthcheck
description: "Проверка здоровья документации проекта судом, а не машиной: не разошлись ли документы между собой и с кодом. Зовёт двух агентов на весь канон разом — doc-consistency (один факт в двух домах, прямое противоречие, поведение в architecture.md вместо спек, ADR без парного статуса, число без происхождения) и doc-code-drift (протухший факт: имя ветки, команды, пути, зависимости поимённо, настройки с числом, единые точки, capability). Разбирает урожай порциями: строка на замену идёт в документ сразу, работа больше абзаца становится задачей. Прогон оставляет след — ключ [docs] healthcheck_last в .av-dev.toml, — и по нему синк документации считает, сколько задач сделано с прошлой сверки, и выдаёт сигнал строкой. Использовать, когда с прошлой сверки сделан десяток задач, когда вернулись к проекту после перерыва, перед тем как опереться на документ в решении, а также шагом adopt и upgrade. Дорого — не на каждой задаче. Раскладку и версию раскладки проверяет скилл av-dev:canon, язык документов — агент doc-wording."
---
# Здоровье документации
Проверяет то, **чего машина не видит**: разошлись ли документы между собой и с
кодом. Раскладка, версия, битые ссылки, нетронутые плейсхолдеры — это `canon
check` и его скрипт; здесь начинается там, где кончается `docs.py`.
Разрез проверяемый: **машина сверяет форму, этот скилл — утверждения**. «В
`architecture.md` есть раздел» проверит скрипт. «В `architecture.md` написано,
что зависимость одна, а в манифесте их три» — суждение, и его выносит агент.
## Когда звать
**Зовёт человек**, но признак наблюдаемый, а не календарный:
- **с прошлой сверки сделан десяток задач.** Документы протухают ровно от
сделанной работы: переименованная цель сборки, ушедшая зависимость, второй
способ делать то, что обзор объявил единственным, факт, дописанный в
`architecture.md` и уже живущий в `CLAUDE.md`. **Этот признак считается, а не
вспоминается**: счёт ведёт синк документации по следу прошлого прогона и
выдаёт строкой на каждой сделанной задаче (`av-dev:doc-sync`, раздел «Сигнал
сверки»);
- **вернулись к проекту после перерыва** — прежде чем опираться на написанное;
- **перед тем как опереться на документ в решении**, если оно дорогое;
- шагом `adopt` и шагом `upgrade` — их зовёт скилл `canon` сам.
**Не на каждой задаче и не на каждом синке документации.** Цена реальная:
`doc-consistency` идёт на `opus`, потому что сличение утверждений — суждение;
`doc-code-drift` хоть и на `sonnet`, но читает репозиторий целиком. Прогон по
каждой сделанной задаче был бы самой дорогой церемонией процесса, а находок дал
бы почти те же: документы расходятся не с одной задачи, а с десятка.
Прежде оба звались шагом сессии между спринтами. Спринтов нет, и **момент
пришлось назвать заново** — иначе их не звал бы никто, кроме разовых `adopt` и
`upgrade`, то есть на живом проекте никогда.
## Чего может не быть
**Копия.** Дом правила — `shared/absence.md` в репозитории плагина.
Правится дом, а не этот файл.
<!-- копия: отсутствие из av-dev/shared/absence.md -->
**Скилл не вправе считать раскладку проекта полной.** Части заводятся порознь и
живут порознь; каждая узнаётся своим следом:
| Чего нет | Как видно | Чего теперь не делает никто |
| --- | --- | --- |
| настройки av-dev | нет `.av-dev.toml` в корне | проект под процесс не заводился; версии нет, настроек нет |
| документы канона | нет `docs/` | проектную конкретику брать неоткуда — темы, инварианты, прецеденты |
| учёт работ | нет каталога задач | запись остаётся владельцу: назови её текстом в докладе |
| источник требований | нет `openspec/config.yaml` | цикл SDD не запускается: спеки не с чем сверять |
**Свой скилл зовётся полным именем** — `av-dev:canon`, `av-dev:task-track`,
`av-dev:code-review`. Короткое имя может разрешиться в устаревшую проектную
копию из `.claude/skills/`, и подмены не будет видно ни в докладе, ни в
поведении.
**Внешний плагин может не стоять.** Их два: `opsx:*` — цикл SDD, и
`av-dev-git:commit` — сообщения коммитов. Путь в дерево чужого плагина не
пишется никогда: `$CLAUDE_PLUGIN_ROOT` ведёт только в своё дерево, а
вычисленный от него путь к соседу либо не откроется, либо откроет чужую
установку. Нужен чужой справочник — зови владеющий им скилл, он прочитает его
сам.
**Отсутствие — исход, а не поломка.** Назови строкой доклада, чего теперь не
делает никто, и продолжай работу. Молчать нельзя: пропуск неотличим от
сделанного. Выдумывать обходной путь нельзя тоже.
**Присутствие узнаётся следом в проекте, а не объявлением.** Перечня того, что
здесь заведено, проект не ведёт — он разошёлся бы с действительностью молча.
<!-- /копия: отсутствие -->
Здесь сосед один: `av-dev:task-track`, когда находка тянет на задачу. Его нет —
находки остаются списком в докладе, и это говорится строкой.
## Пачка — весь канон, и это не расточительство
Оба агента зовутся **на весь канон разом**, а не на пачку, отобранную работой.
Когда пачку отбирала работа, без присмотра оставалось ровно то, чего работа не
касалась: правка, отменившая решение, живёт в одном документе, а парный статус
нужен в другом; факт, продублированный год назад, не попадёт ни в один диапазон
диффа. Канон мал — он читается целиком, и цена этого известна заранее.
## Кого зовёшь и что передаёшь
| Агент | Что смотрит | Читает | Модель |
| --- | --- | --- | --- |
| `doc-consistency` | смысловой дубль, прямое противоречие между документами, поведение в `architecture.md` вместо спек, ADR без ссылки и парного статуса, число без происхождения, заглушка вместо честной строки | `docs/`, `openspec/` | `opus` |
| `doc-code-drift` | протухший факт: имя ветки, команды, пути, зависимости поимённо, настройки с числом, единые точки проекта, capability | весь репозиторий | `sonnet` |
**`doc-code-drift` обязан получить раздел запретов `CLAUDE.md`.** Он гоняет
команды — только читающие, — и без перечня запретов не знает, чего в этом
проекте запускать нельзя. Не передал — он либо остановится, либо тронет то, чего
трогать не следовало.
**Судит не тот, кто писал.** Ни один из двоих ничего не правит: оба возвращают
готовые формулировки, подставляешь ты. Самопроверка документа слабее всего ровно
там, где формулировка казалась удачной при написании.
Одного из двух можно позвать отдельно — но **скажи в докладе, кого именно
позвал**. Доклад, умолчавший об этом, читается как «сверено целиком».
## Разбор урожая
Находки — обычный материал правки, и разбирать их надо **порциями**, а не одним
заходом: тридцать находок подряд получают «принято» не потому, что верны, а
потому, что разбор затянулся.
По каждой находке ровно три исхода:
1. **Строка на замену** — правь документ сразу. Формулировка уже готова, спорить
не с чем, и откладывание превращает её в задачу дороже самой правки.
2. **Работа больше чем на абзац** — задача типа `chore`. Заводит её **не этот
скилл**: вызови Skill `av-dev:task-track`, у него свой формат, дедупликация
против беклога и кладбища. Каталога задач в проекте нет — отдай списком в
докладе и скажи это строкой.
3. **Не находка** — агент ошибся, документ прав. Скажи это прямо: неразобранная
находка и отклонённая различаются, и вторая экономит время на следующем
прогоне. Класс ошибок, который повторяется, идёт в `docs/review.*`, раздел
настройки, — там дом типовых ложноположительных.
## След прогона
**Последним шагом прогон правит `.av-dev.toml`** — ключ `healthcheck_last` в
секции `[docs]`: хеш коммита `HEAD` и дата комментарием рядом. Состав ключей —
[канон](../canon/references/canon.md), раздел `.av-dev.toml`; правится **строка**,
а не файл целиком.
**Секцию и имя ключа не выбирай сам.** Неизвестный ключ `.av-dev.toml` — отказ
кодом 3, а не пропуск: ключ, заведённый мимо константы скрипта-владельца, роняет
`docs.py`, `tasks.py` и гейт проекта разом. Этот ключ там уже назван
(`DOCS_KEYS` в `av-dev/skills/canon/scripts/docs.py`), а любой другой пришлось бы
заводить правкой скрипта.
**Без следа признак «десяток задач» не считается никем.** Так и было: сверку
звали по памяти, то есть не звали — тот же прозаический триггер, что дал 6
записей ADR на 43 изменения. След превращает признак в число, которое
`av-dev:doc-sync` считает командой
`git rev-list --count <last>..HEAD -- openspec/changes/archive` и говорит вслух
на каждой задаче.
Ключ **необязательный и заводится сам** — первым же прогоном сверки; проекту для
этого делать нечего. Его отсутствие значит «сверки не было ни разу», и синк
говорит это отдельной строкой.
**Правку следа коммитит тот, кто позвал прогон.** Своего коммита у скилла нет:
он правит документы, заводит задачи и ставит след — всё это уезжает одним
коммитом разбора, и `last` в нём указывает на **прежний** `HEAD`, то есть на
состояние, которое сверяли. Оставить правку незакоммиченной нельзя: счёт пойдёт
от коммита, которого в истории нет.
**Позвал одного агента из двух — след всё равно ставится, но в докладе назван
неполным.** Иначе следующая сверка отсчитывалась бы от прогона, который смотрел
половину.
## Доклад
- **Кого позвал** — обоих или одного, и почему одного.
- Находки по каждому агенту: сколько, что поправлено сразу, что стало задачей
(со слагами), что отклонено и почему.
- **Границы покрытия**: что смотрели и чего не смотрели. У `doc-code-drift` она
идёт из его собственного отчёта — перечень фактов у него закрытый, и он
называет, какие из них проверить было нечем.
- Канона в проекте нет вовсе — это исход, а не пустой прогон: скажи строкой и
предложи `av-dev:canon`.
## Чего этот скилл не делает
- **Не проверяет раскладку, версию и ссылки** — это `canon check`, там машина.
- **Не судит язык** документов: залог, англицизмы, жаргон, термин без дома — это
агент `doc-wording`, и зовут его отдельно, по пачке правленных документов.
Звонящие у него названные — последний заход синка в `av-dev:doc-sync`, шаг
вычитки сценария разведки (`av-dev:code-resolve`), шаг 9 `av-dev:doc-init` и
шаг вычитки в обоих режимах `canon`, — просто ни один из них не здесь. У него
другой ритм: он нужен там, где текст только что писали, а
не там, где он год лежал. Оркестровать его нечем — он один и работает по
названному списку.
- **Не правит документы за агентов** — они возвращают формулировки, решение
подставить принимает человек или ты по его правилу.
- **Не заводит задачи** — этим владеет `av-dev:task-track`.
- **Не решает, когда себя звать.** Признак считает синк и говорит строкой; часы
на прогон тратит человек своим словом.
+167
View File
@@ -0,0 +1,167 @@
---
name: doc-init
description: "Завести новый проект — сессия вопросов и ответов по свободному описанию замысла, из которой рождается первичная документация по канону av-dev: паспорт, CLAUDE.md с инвариантами и командами, модель угроз с периметром и скелет остальных документов; первый план работ собирает интервью, а записывает его вызовом скилла av-dev:task-track — беклог принадлежит учёту работ. OpenSpec заводит не сам, а вызовом скилла av-dev:code-openspec — каталог принадлежит конвейеру. Использовать, когда начинают новый проект с нуля, когда есть только текст «что мне нужно и почему» и надо превратить его в рабочую документацию, когда просят провести стартовое интервью по брифу. Проект, где документация уже как-то ведётся, переводит скилл canon."
---
# Заведение нового проекта
Вход — свободный текст «что мне нужно и почему». Выход — канон документов, с
которого дальше работают все остальные скиллы.
**Определение канона — [канон](../canon/references/canon.md).** Прочитай его до
первого вопроса: интервью идёт по слотам канона, а не по вкусу. Что класть в
каждый файл — [скелеты](../canon/references/skeletons.md); не выдумывай заглушки
своей формы, `docs.py` узнаёт только плейсхолдер оттуда.
## Что `init` физически не может произвести
В новом репозитории **нет кода**, а `architecture.md`, `database.md`,
`conventions/` и `research/` выводятся из него. Сочинить их на старте — значит
проектировать вперёд реальности, и написанное протухнет раньше первой задачи.
Поэтому `init` заполняет то, что человек знает **до первой строки кода**:
| Заполняется | Остаётся скелетом с честной строкой |
| --- | --- |
| `passport.md` | `architecture.md` |
| `CLAUDE.md` | `database.md` |
| `security.md` | `conventions/` |
| `.av-dev.toml` | `research/`, `adr/` |
| | `review.md` — журнал пуст, настройка появится с первым ревью |
Честная строка информативна, а не «TBD»: «архитектуры пока нет: кода нет,
заводится первой задачей». Проход читает её как факт.
**`tasks/BACKLOG.md` в таблице нет намеренно.** Первый план работ `init`
собирает интервью (блок 6), но записывает его не он: каталогом задач владеет
`av-dev:task-track`, и это шаг 7. Человек от каталога задач отказался — план
остаётся списком в докладе, беклога в проекте не появляется, и это говорится
строкой.
## Порядок интервью — зависимость, а не удобство
Каждый блок опирается на ответ предыдущего; переставлять нельзя.
1. **Цель и потребители.** Ради чего это; кто пользуется — список закрытый, и
он определяет, что считать нужным, а что интересным.
2. **Чем это НЕ является и мера успеха.** Граница домена — критерий, по
которому потом судят в теме `architecture` о переносе понятия. Мера — по чему
поймём, что удалось.
3. **Периметр и недоверенный вход.** Открыт наружу или контур доверенный; что
приходит извне и каким каналом; что чувствительнее чего. Контур ещё не
развёрнут — назови **оба** периметра, целевой и сегодняшний.
4. **Стек, хранилище, необратимое.** Чем пишем и почему; где данные; что в этом
проекте нельзя откатить — деплой, выкладка наружу, перезапись данных.
5. **Чем краснеет гейт.** Какие проверки обязательны; что красит безусловно;
чего в гейте намеренно не будет и кто тогда это гоняет.
6. **Первые шаги стройки.** Новый проект по определению начинается со стадии
`build`: приложения ещё нет. Собери **список от базы к деталям** — что нужно
сделать, чтобы приложение заработало, — и обоснуй **порядок**: он значит
зависимость, а не важность. Пять-десять шагов достаточно: план дописывается
по ходу стройки, и это законно.
### Как вести
- **Не больше трёх вопросов за итерацию** (`AskUserQuestion`), рекомендация
первым вариантом. Между итерациями применяй уже решённое.
- **Сперва вычитай ответы из брифа.** Если ответ уже есть в тексте, вопрос не
задавай — покажи своё прочтение и спроси, верно ли.
- **Не выдумывай четыре вещи:** периметр, что необратимо, измеренные числа и
адресата дорогой проверки. Их из замысла не вывести. Не сказано — пиши
«неизвестно» с пометкой, что ждёт ответа.
- **Развилка замысла — человеку, механика — сама.** Имена файлов, слаги, порядок
строк не выноси.
## Чего может не быть
Два шага порядка работы — вызовы чужого: OpenSpec заводит конвейер, каталог задач
ведёт скилл задач. Ни того, ни другого `init` не делает руками.
**Копия.** Дом правила — `shared/absence.md` в репозитории плагина.
Правится дом, а не этот файл.
<!-- копия: отсутствие из av-dev/shared/absence.md -->
**Скилл не вправе считать раскладку проекта полной.** Части заводятся порознь и
живут порознь; каждая узнаётся своим следом:
| Чего нет | Как видно | Чего теперь не делает никто |
| --- | --- | --- |
| настройки av-dev | нет `.av-dev.toml` в корне | проект под процесс не заводился; версии нет, настроек нет |
| документы канона | нет `docs/` | проектную конкретику брать неоткуда — темы, инварианты, прецеденты |
| учёт работ | нет каталога задач | запись остаётся владельцу: назови её текстом в докладе |
| источник требований | нет `openspec/config.yaml` | цикл SDD не запускается: спеки не с чем сверять |
**Свой скилл зовётся полным именем** — `av-dev:canon`, `av-dev:task-track`,
`av-dev:code-review`. Короткое имя может разрешиться в устаревшую проектную
копию из `.claude/skills/`, и подмены не будет видно ни в докладе, ни в
поведении.
**Внешний плагин может не стоять.** Их два: `opsx:*` — цикл SDD, и
`av-dev-git:commit` — сообщения коммитов. Путь в дерево чужого плагина не
пишется никогда: `$CLAUDE_PLUGIN_ROOT` ведёт только в своё дерево, а
вычисленный от него путь к соседу либо не откроется, либо откроет чужую
установку. Нужен чужой справочник — зови владеющий им скилл, он прочитает его
сам.
**Отсутствие — исход, а не поломка.** Назови строкой доклада, чего теперь не
делает никто, и продолжай работу. Молчать нельзя: пропуск неотличим от
сделанного. Выдумывать обходной путь нельзя тоже.
**Присутствие узнаётся следом в проекте, а не объявлением.** Перечня того, что
здесь заведено, проект не ведёт — он разошёлся бы с действительностью молча.
<!-- /копия: отсутствие -->
Оба скилла в этом же плагине и разрешаются всегда; чем оборачивается отказ от
того, что они заводят, — на самих шагах 3 и 7. Заведение проекта из-за этого не
останавливается: проект без OpenSpec и без учёта задач законен.
## Порядок работы
1. Прочитай бриф целиком. Выпиши, на какие блоки интервью ответ уже есть.
2. Проведи интервью итерациями по ≤3 вопроса.
3. **OpenSpec — вызови Skill `av-dev:code-openspec`.** Он заводит каталог и
заменяет пример в `config.yaml` настройкой. Делается это **до первого
документа**: без `openspec/` не работают ни `opsx:propose`,
ни сверка требований. Каталог принадлежит конвейеру, а не канону, поэтому
здесь только вызов — ни команды, ни формы файла `init` не знает.
**Человек от OpenSpec отказался** — проект живёт без него законно: строка
доклада, и дальше; `docs.py check` о каталоге тоже промолчит. Цикл SDD в
таком проекте не запускается, и это надо назвать, а не обойти.
4. Заведи `.av-dev.toml` в корне с текущей версией раскладки — число берётся из
`docs.py version`, а не из памяти.
5. Напиши заполняемые документы. **Бриф переезжает в `passport.md`** и
отдельным файлом не остаётся: два дома для одного замысла разойдутся на
первом же уточнении.
6. Заведи скелет остальных по [скелетам](../canon/references/skeletons.md) —
каждый с честной строкой.
7. Каталог задач и первые шаги — **вызови скилл `av-dev:task-track`**: он владеет
форматом задач и стадией (`init --stage build`). Не разрешился — учёт задач
остаётся владельцу, и это тоже строка доклада.
8. `docs.py check` из скилла `canon` — до отсутствия дрейфа. Замечания о
незаполненных плейсхолдерах остаются: их закрывает не `init`, а работа.
9. **Вычитай написанное — агент `doc-wording`**, по пачке заполненных документов
(`passport.md`, `CLAUDE.md`, `security.md`). Здесь он нужен сильнее, чем где
бы то ни было: весь текст сочинён только что и по свободному брифу человека, а
бриф — это как раз залог, оценки без факта и жаргон. Скелеты с честной строкой
в пачку не клади, вычитывать в них нечего. Находки — готовые формулировки,
подставляешь их ты.
10. Покажи человеку, что получилось, и **отдельным списком** — что выведено из
брифа, что предположено, что осталось неизвестным. Правят по этим строкам.
## Что дальше
- Содержимое канона по ходу разработки ведёт скилл `doc-sync`.
- Раскладку проверяет `canon check`.
- Первую задачу берёт конвейер проекта; `architecture.md` и `conventions/`
наполняются его шагом синка, а не заранее.
## Чего этот скилл не делает
- **Не проектирует систему.** Архитектура выводится из кода, а не наоборот.
- **Не пишет код** и не заводит сборку.
- **Не переводит существующий проект** — это `canon adopt`. Признак: в
репозитории уже есть документация или беклог в какой-то раскладке.
- **Не решает за человека**, что важно: цель, границы и периметр — его ответы.
+357
View File
@@ -0,0 +1,357 @@
---
name: doc-sync
description: "Вести содержимое документов канона по ходу разработки. Правки двух родов, и спрашивается один: отражение сделанного (вливание дельт, миграция в database.md, компонент в architecture.md) пишется молча, а новая запись и новая норма (ADR, правило в conventions, записка в research, инвариант CLAUDE.md, периметр security.md, граница passport.md, дефект в review.md) только предлагается — пишет её второй запуск после слова человека. Построчный отчёт по каждому документу остаётся: каждый назван либо правкой, либо предложением, либо отрицанием с причиной. ADR и записка разведки — промоут цитатой из архивного design.md или записки, а не второе сочинение. Синк же считает и выдаёт строкой сигнал сверки: сколько задач сделано с прошлого прогона av-dev:doc-healthcheck, читая след в ключе [docs] healthcheck_last. Использовать, когда задача сделана и надо обновить документацию, когда просят завести ADR или записать решение, занести находку о внешних данных, записать проскочивший дефект, разгрузить разросшуюся архитектуру. Раскладку и соответствие канону проверяет скилл av-dev:canon."
---
# Ведение содержимого канона
Скилл владеет **содержимым** документов канона; раскладкой владеет `canon`.
Определение канона и роли документов — [канон](../canon/references/canon.md),
здесь не пересказывается.
Главный вызывающий — **шаг синка документации в конвейере задачи**: скилл
`av-dev:code-resolve` зовёт этот по имени. Задачу ведут не конвейером —
документация ведётся тем же скиллом вручную.
## Правило, из которого всё следует
**Принуждённое отрицание.** Синк обязан назвать **каждый** документ канона: либо
чем он обновлён, либо «не требуется, потому что…». Нетронутые группируются одной
строкой с общей причиной.
Причина, по которой правило именно такое, измерена: у ADR был список триггеров
прозой — и он дал **6 записей на 43 изменения**. Прозаический триггер, который
некому проверить, не срабатывает. Отличить «не написал» от «написал, что не
требуется» можно только тогда, когда отрицание обязательно.
Это тот же приём, что «границы покрытия» в отчёте ревью и «пустое называется
пустым» в каноне.
## Два рода правок, и спрашивается один
Второе правило, поперёк первого: **пройти по всем документам обязан ты, а
завести новое — человек**. Признак проверяемый и читается одним вопросом: **что
станет с документом, если правку не сделать**.
<!-- дом: синк-род-правки -->
- **Отражение** — документ уже описывает эту вещь, и без правки он **станет
ложным**: миграция написана, а `database.md` её не знает; компонент заведён, а
`architecture.md` перечисляет прежние. Такая правка ничего не решает, она
договаривает решённое на чекпоинте и уже стоящее в коде. **Пишется молча.**
- **Новое** — в каноне заводится запись или норма, которой не было: ADR, правило
в конвенциях, записка в `research/`, инвариант в `CLAUDE.md`, сдвиг периметра в
`security.md`, граница в `passport.md`, дефект в журнале `review.md`. Такая
запись переживёт задачу и свяжет следующие. **Пишется только по слову
человека.**
<!-- /дом: синк-род-правки -->
**Показывается новое одной репликой и одним списком.** Каждый пункт — строкой:
что заведём, куда и на каком основании. Человек отвечает разом, и одобренное
пишет **следующий заход синка** — в цикле задачи это третий такт шага 6
(`av-dev:code-resolve`, `references/solve.md`). **Нового нет — реплики нет**, и
это обычный исход: у большинства задач хвост состоит из одного отражения.
**«По слову» — это по слову, а не вторым вопросом.** Человек уже сказал в этом
прогоне «заведи ADR», сам решил сузить проверки, сам одобрил формулировку
конвенции — слово сказано, и переспрашивать нечего: запись идёт как одобренная, а
в докладе стоит, чьим решением. Предложение существует ради нового, которое
заметил ты, а не ради ритуала.
**Отказ человека — строка доклада и всё.** В документы он не пишется: журнала
отвергнутых ADR и снятых конвенций канон не держит, и заведение такого журнала
здесь было бы ровно тем новым, которого никто не заказывал.
**Отрицание от этого не ослабло.** Документ, по которому нечего предложить,
по-прежнему обязан быть назван — просто раньше отрицание читал отчёт, а теперь
человек, и читает он его **до** того, как что-то написано. Обязанность та же:
пропуск неотличим от «не требуется», пока отрицание не сказано вслух.
## Чек-лист синка
Идёт сверху вниз; каждая строка попадает в доклад.
| Документ | Род | Обновляется, когда | Проверка |
| --- | --- | --- | --- |
| `openspec/specs/` | отражение | всегда при изменении поведения | вливает `opsx:archive` |
| `database.md` | отражение | тронуты миграции | `docs.py check --base` |
| `architecture.md` | отражение | новый компонент, граница, внешняя зависимость, изменилось окружение | `docs.py`: capability без упоминания |
| `adr/` | новое | сработал триггер канона (перечень — [canon.md](../canon/references/canon.md#adr)) | нет — только этот чек-лист |
| `research/` | новое | узнали новое о внешнем формате или данных | нет |
| `security.md` | новое | новый недоверенный вход, токен, путь наружу, сдвиг периметра | нет |
| `conventions/` | новое | находка принята и не специфична для одного места | промоут |
| `review.md` | новое | дефект воспроизведён; сузили или расширили проверку | нет |
| `passport.md` | новое | новый потребитель, сдвиг границы «чем не является» | нет |
| `CLAUDE.md` | новое | изменился инвариант, гейт, запрет, необратимое | нет |
**Разрез в таблице не произволен.** Ложным без правки становится ровно тот
документ, который описывает **состояние системы**, — потому отражений в чек-листе
и мало. Остальные задают норму или хранят память: им не с чем разойтись, пока в
них не написано новое.
Пример доклада:
```
Синк документации.
Отражено, записано:
- openspec/specs/ — влиты дельты change add-bucket-reindex
- architecture.md — добавлен воркер свёртки, ссылка на capability reindex
- database.md — миграция 00006, таблица bucket
Предложено, жду слова:
- adr/ — отказ от внешней очереди в пользу таблицы; источник: архивный
design.md; триггер: намеренный отказ от очевидного подхода
Не требуется: research, security, conventions, review, passport, CLAUDE.md —
периметр не двигался, инварианты те же, новое о внешних данных не узнано.
Сверка документов: с прошлой (a1b2c3d, 2026-07-30) сделано 11 задач — пора
звать av-dev:doc-healthcheck.
```
## Сверка — не здесь, а в `av-dev:doc-healthcheck`
Синк правит документы поодиночке, а расходятся они **между собой**: факт,
дописанный в `architecture.md`, уже живёт в `CLAUDE.md`; периметр в
`security.md` не знает про новый эндпоинт. Поймать это на своей же правке нельзя,
и судит это агент `doc-consistency`.
**Но синк его не зовёт.** Обоими судьями документов владеет скилл
`av-dev:doc-healthcheck`, и зовут их на весь канон разом, а не на пачку,
отобранную работой. Причина в цене: `doc-consistency` на `opus` по каждой
сделанной задаче — самая дорогая церемония процесса, а `doc-code-drift` хоть и на
`sonnet`, но читает репозиторий целиком. К тому же расхождение между двумя
документами по определению требует двух документов, а на большинстве задач синк
правит один.
Что теряется: привязка находки к задаче, которая её породила. Что выигрывается,
кроме денег: синк перестаёт отбирать пачку, и в неё попадают документы,
которых работа не касалась, — расхождение, внесённое правкой в одном месте, там
и живёт.
## Вычитка — наоборот, здесь
**Язык правленого вычитывается тем же прогоном, который его написал, и зовёшь
агента `doc-wording` ты.** Довод обратный доводу про судей: он читает **только
названную пачку**, стоит дёшево и ищет ровно то, что портится в момент письма, —
залог, оценку без факта, жаргон, термин без ввода. Ждать `doc-healthcheck` здесь
нечего: через месяц никто уже не помнит, какую фразу имел в виду автор.
Позови его **последним шагом правки, до коммита**, отдав список файлов, которых
она коснулась, — и назови этот список в промпте: по нему же он судит, известен ли
термин. Находки он отдаёт готовыми формулировками, подставляешь их ты.
**Синк бывает в два захода, и вычитка идёт последним из них.** Вернул непустой
список предложений — правка ещё не кончилась: человек ответит, и второй заход
допишет одобренное. Вычитывать пачку, которая сейчас пополнится, значит платить
за неё дважды. Значит: **предложения есть — вычитку откладываешь до второго
захода; предложений нет — этот заход последний, и вычитка идёт в нём.** Отказ
человека второго захода не отменяет: письма в нём не будет, а вычитка и гейт
будут — иначе правка первого захода уедет в коммит невычитанной.
**Условие вызова — правка, а не синк.** Синк самый частый вызывающий, но не
единственный: разведка (`av-dev:code-resolve`, сценарий разведки) пишет ответ по
одному адресу и синком себя не считает намеренно — вычитка ей нужна ровно та же.
Признак один и читается буквально: **документы правились — зови, ничего не правил
— не зови**.
## ADR — промоут, а не второе сочинение
Обоснование уже написано: `opsx:propose` кладёт `design.md` в каждый change, и
после архивации он лежит в `openspec/changes/archive/<id>/design.md` с разделами
`Context` / `Goals / Non-Goals` / `Decisions` / `Risks / Trade-offs`.
**ADR цитирует решение оттуда и ссылается на источник.** Не пересказывает и не
сочиняет заново.
**Второй законный источник — записка разведки**, и приходит он от скилла
`av-dev:code-resolve`, сценарий разведки: решение, принятое разведкой (намеренный отказ, выбор
подхода, «проверили и не делаем»), `design.md` не имеет по построению — change по
нему не будет никогда. Промоут при этом тот же: цитата и ссылка, но на записку.
Перечень источников закрыт и живёт в [каноне](../canon/references/canon.md),
раздел `adr/`.
**Триггер заведения, форма имени и правило замены — в
[каноне](../canon/references/canon.md), раздел `adr/`.** Здесь они не
повторяются: копия правила расходится с оригиналом на первой же смене версии
канона, а расходится незаметно.
Твоя часть — **применить триггер к этой задаче и сказать вслух, сработал он или
нет**. Строка «adr/ — не требуется: решение рутинное» и есть то, ради чего
чек-лист существует; её отсутствие неотличимо от «забыл посмотреть».
**Запись — новое, и заводится она по слову** (раздел «Два рода правок»).
Сработавший триггер даёт не файл, а строку предложения: какое решение, из какого
источника, каким из трёх триггеров прошло. Своей записи ADR не стоит ничего, а
каталог решений читают как список того, что в проекте всерьёз, — и разбавленный
рутиной он перестаёт им быть.
Порядок работы после «да»: открой источник — архивный `design.md` change либо
записку разведки, — найди в нём решение, проходящее триггер, процитируй его и
причину, сошлись на источник, добавь строку в индекс `docs/adr/README.md`
сверху.
## Чистка `architecture.md`
Обзор не держит поведение — его нормативный дом `openspec/specs/`. **Форма
маркера долга и правило «гейт от них не краснеет» — в
[каноне](../canon/references/canon.md), раздел `architecture.md`.**
Разбирается порциями: раздел вычищает та задача, которая его касается.
Содержимое не выбрасывается, а переезжает — требования в дельта-спеку change,
обоснование в ADR, обзор остаётся строкой со ссылкой на capability.
## Запись в `research/`
Наблюдение о внешнем мире: что реально шлёт источник, чем документация формата
расходится с практикой. **Требование происхождения и правило про расходящееся
число — в [каноне](../canon/references/canon.md), раздел `research/`.**
Твоя часть — заметить, что по ходу задачи узналось новое о внешних данных, и не
дать этому остаться в контексте. Признак: ты правил разбор, опираясь на то, чего
нет ни в одном документе.
## Чего может не быть
Два раздела ниже — запись в `review.md` и промоут в конвенции — берут форму у
конвейера ревью: она принадлежит ему, а не канону. Берут **вызовом скилла**, а не
чтением файла по пути.
**Копия.** Дом правила — `shared/absence.md` в репозитории плагина.
Правится дом, а не этот файл.
<!-- копия: отсутствие из av-dev/shared/absence.md -->
**Скилл не вправе считать раскладку проекта полной.** Части заводятся порознь и
живут порознь; каждая узнаётся своим следом:
| Чего нет | Как видно | Чего теперь не делает никто |
| --- | --- | --- |
| настройки av-dev | нет `.av-dev.toml` в корне | проект под процесс не заводился; версии нет, настроек нет |
| документы канона | нет `docs/` | проектную конкретику брать неоткуда — темы, инварианты, прецеденты |
| учёт работ | нет каталога задач | запись остаётся владельцу: назови её текстом в докладе |
| источник требований | нет `openspec/config.yaml` | цикл SDD не запускается: спеки не с чем сверять |
**Свой скилл зовётся полным именем** — `av-dev:canon`, `av-dev:task-track`,
`av-dev:code-review`. Короткое имя может разрешиться в устаревшую проектную
копию из `.claude/skills/`, и подмены не будет видно ни в докладе, ни в
поведении.
**Внешний плагин может не стоять.** Их два: `opsx:*` — цикл SDD, и
`av-dev-git:commit` — сообщения коммитов. Путь в дерево чужого плагина не
пишется никогда: `$CLAUDE_PLUGIN_ROOT` ведёт только в своё дерево, а
вычисленный от него путь к соседу либо не откроется, либо откроет чужую
установку. Нужен чужой справочник — зови владеющий им скилл, он прочитает его
сам.
**Отсутствие — исход, а не поломка.** Назови строкой доклада, чего теперь не
делает никто, и продолжай работу. Молчать нельзя: пропуск неотличим от
сделанного. Выдумывать обходной путь нельзя тоже.
**Присутствие узнаётся следом в проекте, а не объявлением.** Перечня того, что
здесь заведено, проект не ведёт — он разошёлся бы с действительностью молча.
<!-- /копия: отсутствие -->
Здесь это значит: документов канона может не быть вовсе — тогда синка нет, и
это исход, а не повод раскладывать документы по своему усмотрению.
## Запись в `review.md`
Файл держит два раздела с разными сроками жизни — журнал дефектов и настройку
конвейера. **Что в каком и в какой форме — в
[каноне](../canon/references/canon.md), раздел `review.md`**; подробности формы
записи и выбор адреса, куда она ведёт, — у конвейера ревью: `Skill
av-dev:code-review`, его `references/review-journal.md`.
Твоя часть на синке: **дефект пишется сразу**, а не «потом, когда починим».
Со временем теряется не факт, а то, почему дефект не поймали, — единственное,
ради чего журнал есть.
«Сразу» и «по слову» здесь не спорят: запись — новое, и она идёт предложением, но
**предложением этого прогона**, а не следующего. Отложить её до «когда починим»
нельзя ни с чьего согласия: чинится дефект, а теряется причина промаха.
**Решение сузить проверки** (перестали звать проход, переселили его в другой
скилл) обязано попасть в раздел настройки, а не остаться в отчёте ревью. Второй
раз оно не спрашивается: такое решение принимает человек по определению, и слово
по нему уже сказано — сказано тогда, когда проверку сузили.
## Промоут в конвенции
Находка → конвенция → правило линтера → **удаление из прозы**. Процедура целиком
принадлежит конвейеру ревью — его `references/promote.md`, читается через
`Skill av-dev:code-review`; роль каталога конвенций — в
[каноне](../canon/references/canon.md). **Прогон идёт вне конвейера**
(находку принесли руками) — три шага всё равно твои, просто без его процедуры:
сформулируй правило,
поищи, чем оно механизируется, и вычеркни прозу, если механизировалось.
Твоя часть — **третий шаг, который пропускают чаще всего**: правило заработало,
а формулировка осталась в прозе, и проход продолжает проверять уже проверенное.
На синке это отдельная строка: «conventions/ — правило X механизировано,
формулировка удалена» либо «не требуется».
**Конвенция — самое дорогое из нового, и на синке она только предлагается.**
Одна её строка становится входом каждого следующего прогона ревью и критерием
для всех будущих задач; находка, доехавшая до конвенции по инерции хвоста, потом
годами разменивается на внимание прохода. Предложение называет **проверяемое
свойство и проход, который его нашёл**, — по этой паре человек и решает.
**Шаг 2 в хвост задачи не помещается.** Механизация правила — конфиг линтера или
сканер, плюс приведение кода к зелёному — это работа размером с задачу, и делать
её попутно значит удваивать чужой прогон. Согласованный промоут даёт строку
конвенции сейчас и **задачу типа `chore`** на механизацию — заводит её
`av-dev:task-track`, и заводится она тем же словом человека, что и сама
конвенция.
## Сигнал сверки — строка, а не вызов
Сверку документов (`av-dev:doc-healthcheck`) зовёт человек по признаку **«с
прошлой сверки сделан десяток задач»**. Признак наблюдаемый, но считать его было
нечем: следа у сверки не оставалось, и «десяток» держался в чьей-то памяти. Это
ровно тот прозаический триггер, который дал 6 записей ADR на 43 изменения, — и
здесь он не срабатывал по той же причине.
**След оставляет сама сверка** — ключ `healthcheck_last` в секции `[docs]`
файла `.av-dev.toml` (состав ключей — [canon.md](../canon/references/canon.md),
раздел `.av-dev.toml`). **Считает синк**, и вот чем:
```sh
git rev-list --count <last>..HEAD -- openspec/changes/archive <каталог задач>
```
Считаются коммиты, тронувшие **архив change или каталог задач** (его путь — ключ
`[tasks] dir`). Оба пути выбраны потому, что доведённая до конца задача оставляет
след хотя бы в одном: решение архивирует change, а обслуживание и разведка change
не заводят вовсе и видны только закрытием — правкой индексов учёта. Считать один
архив значило бы не считать `chore` и `research`, то есть на проекте с их
перевесом говорить «звать рано» вечно.
Ни `openspec`, ни каталога задач в проекте нет — считай коммиты
(`git rev-list --count <last>..HEAD`) и **скажи, что считал коммиты**: число
другого рода, и молчаливая подмена сделала бы признак вдвое чувствительнее.
Постановка, пришедшая текстом, следа не оставляет ни там ни там — такие задачи в
счёт не входят, и это тоже говорится строкой, когда прогон шёл текстом.
Строка доклада обязательна всегда, и вариантов у неё три:
- **счёт меньше десятка** — «с прошлой сверки N задач, звать рано»;
- **счёт от десятка** — «с прошлой сверки N задач, пора звать
`av-dev:doc-healthcheck`»;
- **ключа нет** — «сверка документов не проводилась ни разу», и это самый
сильный из трёх сигналов, а не отсутствие данных.
**Сам не зовёшь.** Прогон сверки идёт по всему канону и держит `opus`; решение о
таких часах принимает тот, кто их оплачивает. Синк, позвавший её сам, превратил
бы самую дорогую проверку процесса в церемонию хвоста задачи — против чего она и
вынесена в отдельный скилл.
## Чего этот скилл не делает
- **Не проверяет раскладку** — это `canon`.
- **Не заводит недостающие документы** — их скелет кладёт `canon adopt` или
`doc-init`.
- **Не сочиняет содержание.** Нечего записать — так и пишется, честной строкой.
- **Не переоформляет документы «заодно»**: правится то, чего коснулась работа.
- **Не заводит новое молча** — ни ADR, ни конвенцию, ни записку. Молча идёт
только отражение, и признак у него один: без правки документ станет ложным.
- **Не зовёт сверку документов** — считает и говорит строкой; зовёт человек.
+279
View File
@@ -0,0 +1,279 @@
---
name: task-groom
description: "Груминг беклога — интерактивный разбор, отвечающий на два вопроса: что сейчас самое важное и что перестало быть важным. Ответ записывается порядком строк в беклоге: первая строка — то, что делают следующим. Разбирает накопившиеся вопросы, переоценивает задачи порциями по 5–8 (сделано попутно, отменено решением, слилось с соседней, подешевело, разрослось, стало сырьём), закрывает отжившее с причиной и расставляет очередь. Использовать, когда просят разобрать беклог, расставить приоритеты, решить «что делать дальше», провести груминг или переоценку, а также когда вернулись к проекту после перерыва и надо понять, где остановились. Формат и содержимое записей — скилл av-dev:task-track; выполнение задачи — конвейер проекта."
---
# Груминг: что важно, что перестало
Скилл отвечает на **два вопроса**, и всё, что не служит им, — не его работа:
1. **Что сейчас самое важное?**
2. **Что перестало быть важным?**
Ответ на оба **записывается порядком строк в беклоге**: первая строка секции —
то, что делают следующим; то, что перестало быть важным, из беклога уходит с
причиной. Приоритет — свойство очереди, а не задачи, и живёт он в индексе
(правило 4 скилла `task-track`). Груминг — единственное место, где очередь
назначается человеком.
**Скилл интерактивный.** Он не «приводит беклог в порядок» сам: суждение о
важности принадлежит человеку, и весь ход — это подготовленные развилки с
рекомендацией. Что решается фактом (сделано, отменено, дублируется), решается
без вопросов и показывается списком.
Форматом и содержимым записей владеет скилл `task-track` — груминг зовёт его
операции, а не правит файлы руками. Выполнением задачи — конвейер проекта.
## Три правила, из которых всё следует
1. **Порядок назначает человек, машина его не выводит.** Ни давность, ни тип, ни
размер секции приоритетом не являются. Единственное место в очереди,
назначенное не человеком, — конец секции у сырья, и оно из очереди изъято
(`task-track`, правило 4).
2. **Порция важнее охвата.** Тридцать задач за заход — это усталость и
штамповка: последние десять получат «оставить» не потому, что живы, а потому,
что разбор затянулся. Лучше две честные порции, чем один полный проход.
3. **Причина уезжает в запись.** Всё, что решено здесь, оставляет след:
`--reason` у закрытия и переноса, ответ в теле задачи, строка в докладе.
Решение, оставшееся в переписке, будет принято заново через месяц.
## Груминг — операция доработки
**Стадия проекта решает, применим ли груминг вообще** (дом стадии —
[`task-track`, «Две стадии»](../task-track/SKILL.md#две-стадии); посмотреть —
`tasks.py stage`).
На **доработке** он и есть основная гигиена: беклог пополняется извне и
вразнобой, порядок значит важность, и назначить её может только человек.
На **стройке** оба вопроса скилла отвечены заранее. «Что сейчас самое важное» —
первая строка плана, и назначил её не приоритет, а зависимость: переставить её
значит сломать стройку. «Что перестало быть важным» возникает не порциями, а
разом — когда меняется замысел, — и тогда пересматривается **план целиком**, а
не 5–8 задач из середины. Порционный разбор здесь вреден: он вынимает шаги из
списка, порядок которого и есть его содержание.
Поэтому на стройке скилл говорит это строкой и **отсылает к другой работе**:
[пересмотр плана целиком](../task-track/SKILL.md#пересмотр-плана-стройки) —
сценарий скилла `task-track`, гигиена полей — тоже его, а исчерпанный беклог
значит переход (`tasks.py stage support`). Четыре вещи он делает и на стройке,
потому что от стадии они не зависят: `tasks.py check --fix`, разбор
накопившихся вопросов, закрытие сделанного попутно и **возврат неудавшейся
приёмки** (`reopen`).
**Возврат приёмки от стадии не зависит вовсе, и это надо сказать отдельно.**
Приёмщик и исполнитель у нас совпадают, и опор против этого две: независимый
отчёт ревью и `reopen`. Вторая привязана к грумингу только по привычке — заметил,
что закрытая задача сделана не тем, чем обещала, возвращай сразу, на любой
стадии и в любой момент.
## Когда груминг созрел
**Зовёт человек.** Скилл сам себя не назначает, но обязан **напоминать**, и
признак наблюдаемый, а не календарный:
- в беклоге появились записи, которых человек ещё не видел (заведены по ходу
работы, урожаем ревью, разбором находок);
- на верхних строках очереди есть задача с открытым вопросом — очередь
показывает то, что взять нельзя;
- `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/>порциями по 58"]
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:doc-healthcheck`), и иди дальше. Документов канона в проекте нет —
сверять нечем, и это тоже строка.
## Интерактив
- Вопросы — через `AskUserQuestion`, **не больше трёх за раз**. Порция в 58
задач обычно даёт больше трёх суждений: веди несколько итераций по ≤3, а не по
одному вопросу на задачу и не одним перегруженным запросом.
- К каждому варианту — **предварительное суждение, рекомендация первым
вариантом**: «предлагаю выкинуть, потому что …». Возразить дешевле, чем судить
с нуля.
- Всё, что решается фактом, решай сам и показывай списком в докладе.
- **Останавливайся на границе порции**, даже если «ещё чуть-чуть осталось».
Между порциями — промежуточный доклад.
Примеры итераций, отбор порции, храповик на залежавшихся —
[references/portions.md](references/portions.md).
## Стимулы, которые процесс создаёт
Правило, которое можно обойти в свою пользу, будет обойдено.
**Приёмщик и исполнитель совпадают, и это надо назвать вслух.** Задачу закрывает
тот же агент, который её и сделал. Груминг приёмкой не занимается — отдельного
ритуала у неё нет, — и настоящих опор остаётся две:
- **независимый отчёт ревью** — артефакт, написанный не исполнителем: отчёт
триажа в
`openspec/changes/archive/<id>/review/` (до архивации — `changes/<id>/review/`);
- **`reopen <слаг> --reason`** — закрытие не окончательно. Заметил, что закрытая
задача сделана не тем, чем обещала, — возвращай, это штатная операция, а не
скандал, и ждать груминга она не требует (на стройке его и не будет). Индексы под git: `git log -p` по беклогу показывает,
что и когда закрыто, потому что закрытие коммитится отдельным коммитом учёта.
Известные обходы:
- **Не записать вопрос** на задаче, которую хочется поднять наверх очереди.
Защита: вопросы верхних строк разбираются вне очереди порции, шагом 2.
- **Оставить всё как есть.** Груминг, на котором ничего не сдвинулось и ничего
не закрылось, — это не «беклог в порядке», а не проведённый груминг. Защита:
задача из верхних строк, которую и этот заход оставляет без изменений, **либо
двигается, либо получает записанную причину**, почему её держат.
- **Расставить порядок молча**, без доводов: тогда через месяц он неотличим от
случайного. Защита: причина у каждого движения и строка доклада.
- **Разобрать много и мелко** вместо немногого и важного: тридцать полей гигиены
вместо трёх решений о важности. Защита: гигиена — работа скилла `task-track` и
побочный продукт здесь; доклад называет **решения**, а не правки.
## Слоты проекта
Груминг не знает ни языка, ни сборки, ни CI. Проект дописывает в `CLAUDE.md`:
1. **Что считается сломанным** — какая красная проверка обгоняет развитие.
Не названо — спрашиваем человека, а не решаем сами.
2. **Необратимое** — что спрашивается всегда (тот же слот, что у скилла `task-track`;
дом один).
3. **Ориентир по размеру порции**, если он замерялся. Умолчание — 5–8 задач, и
это **ориентир, а не закон**.
Числа проекта (сколько задач приходит за месяц, каков прирост беклога) — предмет
наблюдения человека, а не константы этого скилла.
## Доклад
- Что просмотрено: N из M, сколько порций, по какому признаку отобраны.
- Вопросы: разобрано N, из них отвечено без человека N, снято тегов N.
- **Что перестало быть важным**: удалено как реализованное (со ссылками), ушло
без реализации (с причинами), понижено до сырья, слито, сменило тип.
- **Что важно сейчас**: верх очереди по каждой секции — слаги в порядке, и по
каждому движению довод одной строкой.
- **Границы покрытия**: сколько задач не трогали и какие именно секции или теги
остались — иначе доклад читается как «беклог разобран».
- `tasks.py check` после правок — результат строкой.
## Чего этот скилл не делает
Не пишет код и не выполняет задачи. Не заводит и не переоформляет записи сам по
себе — формат и содержимое ведёт `task-track` (груминг зовёт его операции). Не решает
за человека, что важно: он готовит развилки и рекомендует. Не принимает
закрытые задачи отдельным ритуалом — `reopen` есть, момента у него нет. Не судит
документы проекта — это скиллы `av-dev:canon` и `av-dev:doc-healthcheck`.
@@ -0,0 +1,156 @@
# Порции, разбор и расстановка
Процедура шагов 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`), **перепиши «зачем»**: «Решено: …» на вопрос
«почему это лежит в беклоге» уже не отвечает. Опустошение раздела — не
уборка, а условие взятия: правило и причина в скилле `task-track`,
[references/task-format.md](../../task-track/references/task-format.md).
**Вопросы на верхних строках очереди разбираются вне очереди порции** — здесь
же, даже если сама задача в порцию переоценки не попала. Иначе правило «задача с
открытым вопросом в работу не берётся» создаёт стимул вопрос не записывать, лишь
бы не вычеркнуть задачу из ближайшей работы.
## Шаг 3. Что перестало быть важным
Цель — выкинуть то, что перестало быть задачей, и вернуть остальному честное
состояние. Не «пересмотреть всё», а «пересмотреть порцию до конца».
### Порция и правило остановки
- **5–8 задач за порцию.** Размер обоснован усталостью, а не пропускной
способностью, и менять его не надо — **надо брать несколько порций**.
- **Отбор порций по порядку:**
1. **свежее** — заведённое с прошлого груминга: оно ещё не проходило ни одной
проверки на нужность. Свежесть меряется git'ом, как и залежалость, — датой
появления файла в истории;
2. дальше **по залежалости**`list --stale`;
3. по потребности — одна секция целиком, один тег (партия ревью), список от
человека.
- **Останавливайся на границе порции**, даже если «ещё чуть-чуть осталось».
Между порциями — промежуточный доклад.
### Что делать с каждой задачей
Сперва то, что не требует ничьего решения:
1. **Проверь, не сделано ли уже.** Задача, реализованная попутно в соседнем
изменении, — самая частая находка. Смотри код, документацию, историю коммитов
по ключевым словам. Удаление «как реализованной» деструктивно и без следа
`REJECTED.md` реализованные не пишутся), поэтому порог улики жёсткий:
`close <slug> --implemented` только имея **конкретный коммит или строку
документа**, закрывающие задачу, и ссылка идёт в доклад. Есть лишь косвенные
признаки — не удаляй сам, вынеси в пачку вопросов. Сделана частично → задача
сжимается до остатка: тело правишь редактором, заголовок и «зачем» — через
`edit`.
2. **Проверь, не отменена ли решением.** Документ, ADR или архивное изменение
мог закрыть вопрос иначе — тогда `close <slug> --reason "<ссылка на
решение>"`. Задача закрывается не только коммитом.
3. **Проверь пересечения.** Две задачи об одном — содержимое в одну, вторую
`close <slug> --reason "слита с <другой-слаг>"`. Смотри **шире порции**:
заведение сверяет новое против уже лежащего, но никогда не пересматривает
уже лежащее, и две задачи с одной причиной могут лежать рядом месяцами.
4. **Пере-кластеризуй по общей причине.** Несколько задач, оказавшихся симптомами
одного дефекта, сливаются в одну — это находка, которую заведение дать не могло.
5. **Гигиена полей** — протухшее «зачем», вопрос в прозе, снятый ответ, свойство
репозитория в рамках, предписание процесса в теле, тип, разошедшийся с
задачей, границы вместо реализации в разделе «Затрагивает». Список и правила —
в скилле `task-track`. **Груминг — то самое место, где беклог добирает тип и
разделы его схемы:** требовать их на входе значило бы выгонять в заметки то,
что должно лежать задачей, а к взятию в работу они уже обязательны (`ready`).
Блок здоровья `check` печатает, сколько записей готово к взятию, — по этому
числу и видно, добрал ли груминг.
Гигиена — **побочный продукт, а не предмет**. Тридцать полей вместо трёх
решений о важности означают, что груминг не состоялся.
Затем — то, что решает человек:
6. **Жива ли она вообще.** Контекст мог измениться: ушла зависимость, отпал
сценарий, обошли иначе. Здесь и звучит вопрос, выкидывать ли.
7. **Тот ли тип.** Заводилась починкой, а после разбора оказалось, что
поведение никогда и не было заявлено, — это `feature`. Тип, оставшийся от
прошлой формулировки, врёт ровно там, где по нему отбирают, и требует не тех
разделов.
8. **Задача ли это по-прежнему.** Не проходит `ready` по существу, а не по
недописанным разделам → `edit <slug> --type research` и опустошённый раздел
«Вопрос», то есть сырьё; дальше штурм. Разрослась → это несколько задач,
дальше декомпозиция.
9. **Не подешевела ли она.** Сделанная с прошлого раза работа меняет цену
**других** задач: рядом с только что тронутым кодом та же работа стоит меньше.
Это довод и на шаге 4 — задача, внезапно подешевевшая, поднимается в очереди
не потому, что стала важнее, а потому, что окно открыто.
### Храповик на залежавшихся
Сильно залежавшаяся задача — сигнал сама по себе: её либо ни разу не собирались
делать, либо нечем взять. Измеряй наблюдаемым — датой последней правки из git
(`list --stale` ставит такие первыми); счётчик «сколько грумингов пережила»
нигде не хранится.
Задача из верхних строк `--stale`, которую и этот заход оставляет без изменений,
**либо двигается (меняет полку, поднимается в очереди, уходит с причиной), либо
остаётся с явно записанной причиной**, почему её держим (`move <slug> --reason
` — без `--section` секция берётся текущая). Молчаливое «оставить как есть» на
давно неподвижной задаче — это решение не принимать решение; запись причины
превращает его в осознанное и не даёт тому же вопросу всплыть на следующем
груминге.
## Шаг 4. Что важно сейчас — расстановка
Разбирается **верх очереди**, а не весь беклог: первые три-пять строк каждой
секции. Ниже пятой строки порядок всё равно перестаёт что-либо значить.
1. **Покажи текущий верх**`list`, по секциям, в том порядке, в каком строки
лежат.
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`.
## Что делать, если разбирать нечего
Беклог пуст или в нём три задачи и все живые — груминг кончается за минуту, и
это законный исход. Скажи строкой: очередь такая-то, сдвигать нечего. Придумывать
работу, чтобы груминг «состоялся», — ровно тот ритуал без выгоды, от которого
процесс избавлялся.
+744
View File
@@ -0,0 +1,744 @@
---
name: task-track
description: Ведение задач как каталога markdown-файлов (одна задача = один файл в items/ + строка в BACKLOG.md). У каждой задачи есть тип (feature, fix, chore, research), и тип решает, каких разделов она требует и что с ней можно делать. У проекта есть стадия (build — беклог это план стройки, порядок строк значит зависимость; support — очередь правок, порядок значит важность). Заведение записи из диалога, разбор находок аудита/ревью, декомпозиция на независимо полезные части, штурм сырья, гигиена полей, смена стадии и проверка согласованности индекса. Использовать, когда просят добавить задачу или идею, превратить находки ревью в задачи, разбить задачу, проработать идею, поправить формат, объявить стадию или проверить беклог. Каталог отстал от версии раскладки — это скажет tasks.py check, а повышает проект скилл av-dev:canon по общему журналу версий. Расстановка приоритетов и разбор накопившегося — скилл av-dev:task-groom. Не реализует задачи — этим занимается скилл решения задачи.
---
# Задачи
Задачи — каталог markdown-файлов. Одна задача = один файл `items/<slug>.md` плюс
строка в `BACKLOG.md`. Скилл владеет **форматом и содержимым**: заводит,
редактирует, закрывает, разбирает находки ревью, дробит, штурмует сырьё.
Чем он **не** владеет: **очередью** — что делать следующим и что перестало быть
важным, решает скилл `task-groom`, а этот скилл лишь даёт ему операции; и выполнением
задачи — это конвейер проекта.
## Шесть правил, из которых всё следует
Ситуация не покрыта инструкцией — решай по ним.
0. **Стадия решает, что значит порядок строк.** Проект живёт в одной из двух
стадий, и обе ведут один и тот же беклог, но читают его по-разному.
На **стройке** (`build`) беклог это план от базы к деталям: порядок —
зависимость, «раньше нельзя». На **доработке** (`support`) беклог это очередь
правок: порядок — важность, «раньше лучше». Из этого следует остальное —
сколько у беклога секций, как его пополняют, что значит его опустошение и
нужен ли груминг. Стадия объявлена ключом `[tasks] stage`; молчание ответом
не считается, и `check` без неё отказывает.
1. **Беклог гниёт с той стороны, где его пополняют — на доработке.** Заведение
там самая частая операция и с худшим отказом: из одного разговора рождается
пять файлов, а переоценка потом разгребает то, чего не надо было заводить.
Дедупликация и фильтр на входе дешевле любой чистки: заводим только то, что
**не делаем сейчас** и о потере чего пожалеем.
**На стройке правило не применяется**, и это не послабление. Список стройки
пишется вперёд целиком — он и есть замысел, — а фильтр «не заводи то, чего не
делаешь сейчас» запретил бы написать план дальше первого шага. Дедуп остаётся
в обеих стадиях: две записи об одном плохи всегда.
2. **Файл — источник истины, индекс производен.** Разошлись — неправ индекс.
Согласованность механизируема и проверяется командой, а не вниманием: всё,
что ловит `tasks.py check`, не должно попадать ни в чек-лист, ни в промпт.
Поэтому **«зачем» живёт в мете файла**, а строка индекса его лишь
повторяет: пока поле лежало только в индексе, восстановление пропавшей
строки теряло его молча и навсегда. Единственное исключение намеренное:
**порядок строк в беклоге** — он свойство списка, а не задачи, и в файле ему
места нет (правило 4).
3. **Причина переживает запись.** Выкинутая без причины задача вернётся через
квартал тем же текстом. Реализованная оставляет след в коммите — выкинутая не
оставляет ничего, поэтому у неё есть `REJECTED.md`.
4. **Порядок строк — единственное, чего в файле нет.** Он значим в обеих
стадиях, и назначает его человек: на стройке — раскладывая шаги по
зависимости, на доработке — на груминге. Машина порядок не выводит и не
угадывает; всё, что она делает сама, — ставит машинную позицию в **конец**
секции и говорит об этом вслух.
**Дом порядка — индекс, а не файл.** Положи его в файл числом, и два соседних
файла смогли бы утверждать одно и то же место, а строка индекса —
противоречить обоим.
Одно место в очереди назначено **не человеком, а типом**: **сырьё**
(`research` без раздела «Вопрос») стоит в конце своей секции. Его не берут,
и между берущимся оно каждый раз требует открыть файл, чтобы это понять. Раз
это выводится, проверяет и чинит это машина.
5. **Тип решает, что с задачей можно делать.** Тип — вторая ось и первое
поле меты: от него зависит, какие разделы обязательны в теле и берётся ли
запись в работу. Словарь закрыт; ни один тип не подошёл — значит, в записи их
два, и её надо разделить.
## Раскладка
Каталог задач — **`tasks/` в корне репозитория, жёстко.** Он принадлежит этому
скиллу, а не канону документов: учёт работ ведут и в проекте, который к канону
не приведён, и каталога `docs/` там нет вовсе. Внутри
`docs/` задачи лежали до версии канона 11; непереехавший проект скрипт
по-прежнему находит, но новый заводит только в корне.
```
tasks/
items/ задачи файлами, <slug>.md, слаги английские
BACKLOG.md что можно взять. Порядок строк в секции значим,
и значит он разное на разных стадиях
REJECTED.md ушедшее БЕЗ реализации, с причиной и датой
```
**Индекс один.** `REJECTED.md` индексом не считается: он не говорит, где запись
числится, — это кладбище ушедшего.
**Секции беклога называет проект**, и `check` проверяет у них ровно две вещи:
что секция есть хоть одна и что на стройке она **одна**. Смысла секции не несут
— это полки домена (`Ядро`, `Инфра`), — и подгонять их имена под свой вкус
скрипт права не имеет. Поле меты, называющее полку, зовётся **Категория**.
**Заголовок секции отбит пустой строкой с обеих сторон и написан с прописной**;
отбивку правит `check --fix`. Написание секции в мете файлов он тоже правит: имя
секции принадлежит заголовку индекса, файл на неё только ссылается.
**Секции «блокеры» в беклоге нет.** Блокер — это *состояние* (работа не может
продолжаться ни одной задачей), а не полка: он живёт ровно до ответа человека, и
записи в такой секции не успевают жить. Следы блокера остаются вопросами в
файлах задач. Постоянно пустая секция со старой семантикой
«разбираются пачками» противоречила бы правилу «блокер эскалируется немедленно»,
поэтому `init` её заводить отказывается, а `check` о ней говорит. **Проекту,
который переезжает с такой секцией, её надо удалить** — это единственное место,
где это сказано.
**Порядок строк — единственное, чего в файле нет** (правило 4). Отсюда следствие
для всякой машинной правки индекса: восстановленная или перенесённая строка
встаёт **в конец своей секции**, и скрипт об этом говорит. Молчаливая вставка
выдала бы машинную позицию за решение человека — а решение это его.
**У сделанной задачи записи не остаётся** — файл и строка удаляются (`close
--implemented`). Ей хватает коммита и документации проекта; вторая запись была
бы вторым домом для того же факта. Вопрос «что было сделано и когда» отвечается
даром: индекс лежит под git, а закрытие коммитится отдельным коммитом учёта —
`git log -p tasks/BACKLOG.md` отдаёт историю без отдельного журнала.
Куда запись может переехать и какой командой — весь набор переходов:
```mermaid
stateDiagram-v2
state "BACKLOG.md — что берут" as B
state "REJECTED.md — ушла без реализации" as R
state "записи нет — реализована" as D
[*] --> B: add --type feature|fix|chore|research
B --> B: move --after | --first | --section
B --> D: close --implemented
B --> R: close --reason
D --> B: reopen --reason
R --> B: reopen --reason
```
Состояния здесь — **где числится строка**, а не где лежит файл: файл
`items/<slug>.md` не двигается ни на одном переходе. Стрелок «руками» на схеме
нет намеренно — каждый переход это команда, и другого способа его совершить не
существует.
Схема — **сводка**: условия и оговорки живут в тексте разделов, и при
расхождении прав текст.
## Две стадии
**Стадия проекта — ось, и решает она, что значит порядок строк беклога.**
Значения два, дом — ключ `[tasks] stage` в `.av-dev.toml`.
| | `build` — стройка | `support` — доработка |
| --- | --- | --- |
| Порядок строк | зависимость: раньше **нельзя** | важность: раньше **лучше** |
| Секции | ровно одна: список от базы к деталям | полки домена, сколько нужно |
| Заведение | список пишется вперёд целиком | по одной, по мере появления |
| Пустой беклог | план исчерпан, стройка окончена | нормальное состояние |
| Груминг | не применяется; замысел сменился — план пересматривается целиком | основная гигиена, порциями по 5–8 |
| Залежалость | не считается: шаг ждёт своей очереди законно | считается, `list --stale` |
**Стадия называется явно, и молчание ответом не считается.** Без неё порядок
строк нечем прочитать: переставить строку значит на стройке сломать план, а на
доработке — принять решение о важности, и это разные действия. `init --stage`
обязателен, `check` без ключа отказывает, `check --fix` его не подставляет:
какая стадия у проекта, знает человек, а подставленное умолчание соврало бы ровно
там, где по нему принимают решение.
**Секций на стройке одна, и это не педантизм.** Порядок там — зависимость, и
разложенный по полкам список перестаёт быть планом: два шага из разных секций
уже не сравнить. На доработке полки законны — правки независимы, и очередь
внутри полки самостоятельна.
**Переход — событие, а не настройка.** `tasks.py stage support` переносит остаток
беклога в первую новую секцию, правит «Категорию» в файлах и говорит, что порядок
с этого момента значит другое. Датой ему служит коммит: отдельного журнала ради
одной строки не заводится. Признак созревания наблюдаемый — беклог стройки
исчерпан, и `check` об этом напоминает; **запретить переход раньше скрипт не
берётся**: «приложение построено» решает человек, а не счётчик строк.
Обратный переход (`stage build`) разрешён и устроен так же. Он редок — проект
уходит на стройку заново разве что при переделке замысла целиком, — но
запрещать его было бы запретом на то, что иногда и правда случается.
## Чего у задач больше нет
**Тип `goal` и `ROADMAP.md` упразднены.** Цель была зонтиком над параллельными
направлениями: она нужна там, где список работ нельзя выстроить в один порядок,
и очередь идёт поперёк направлений. У проекта, который ведёт один человек, такого
не бывает — на стройке список линеен по зависимости, на доработке правки
независимы, — и зонтик не стоял ни над чем.
Роадмап при этом отвечал на свой вопрос наполовину: «чего ещё не умеет» — это
«что осталось в беклоге», то есть пересказ второго индекса. Вторая половина, «что
уже умеет», живёт в двух домах и без него: нормативное поведение — в
`openspec/specs/`, а когда и в каком порядке оно появилось — в `git log` индекса
и коммитах задач.
Вместе с целью ушли: секция `Готово` (её ответ дают спеки и история), теги
`goal:<слаг>` и `decomposed`, поле меты `Секция` (осталась `Категория`), раздел
`Завершение`, ключи `[tasks] roadmap` и `[tasks] completion_heading`, флаги
`add --goal`, `list --goal`, `list --index`, `edit --goal`, `edit --section` и
`init --roadmap`. Встретились в проекте —
`check` назовёт их поимённо, `check --fix` снимет теги, а запись типа `goal`
оставит человеку: во что она превращается — в задачу или в ничто, — машина не
решает.
**Тип `[epic]` упразднён раньше и не вернулся.** Слишком крупный шаг дробится на
шаги помельче, стоящие в списке подряд.
## Тип записи
**Тип решает, что с задачей можно делать.** Вторая ось скилла — первая стадия.
Перечень осей всего процесса и того, чего каждая **не** решает, —
[shared/axes.md](../../shared/axes.md). Дом типа —
**поле меты `Тип` первой строкой**; эмодзи в заголовке H1 от него производна, её
ставит `add` и чинит `check --fix`.
| Тип | Обязательные разделы | Устав |
| --- | --- | --- |
| ✨ `feature` | `Затрагивает`, `Критерии приёмки` | [task-feature.md](references/task-feature.md) |
| 🐞 `fix` | `Воспроизведение`, `Затрагивает`, `Критерии приёмки` | [task-fix.md](references/task-fix.md) |
| 🧹 `chore` | `Затрагивает`, `Критерии приёмки` | [task-chore.md](references/task-chore.md) |
| 🔬 `research` | `Вопрос`, `Куда ляжет ответ` | [task-research.md](references/task-research.md) |
Берутся в работу все четыре: записи, которую нельзя взять, больше не существует.
Сверх обязательных у любой задачи допустимы `Рамки` и `Вопросы`. Раздел не из
схемы своего типа — **замечание, а не ошибка**: свой раздел законная вольность
проекта, но `Воспроизведение` у `chore` почти всегда значит, что тип проставлен
не тот, и сказать об этом стоит, не запрещая.
**Прежних осей было две, и ортогональность у них была фальшивой.** Тип записи
(`goal`/`idea`/`task`) и род работы (`kind:<род>` тегом) давали двенадцать клеток
произведения, из которых законны были шесть: у цели род запрещён, у задачи
обязателен, у идеи пуст. Плюс алгоритм работы крепится не к `task`, а к `fix` и
`research` — то есть к роду. Оси схлопнуты, тег `kind:` упразднён.
**Тип `idea` упразднён вместе с ними.** Он значил не род работы, а **состояние
незаполненности** — «первый, второй или третий вопрос теста готовности не
отвечается», — а состояние типом быть не может: оно меняется по мере того, как
запись дописывают, а тип меняют командой. Теперь это состояние называется честно:
`research` без раздела «Вопрос» — **сырьё**. В работу не берётся ровно как
прежняя идея, лежит в конце своей категории и отбирается `list --raw`.
Словарь **закрыт**. Открытый разъедется на синонимах — `bug`, `bugfix`, `fix`,
`defect`, — и отбор по типу перестанет отвечать на свой единственный вопрос. Ни
один тип не подходит — это сигнал, что в задаче их два и её надо разделить.
**Требуется тип там, где по нему принимают решение:** `ready` без типа
откажет, потому что не знает, каких разделов требовать. `check` о пропаже только
**напоминает** — беклог, заведённый до появления типа, законен, и переоформлять
его «заодно» здесь не просят.
**Тип не выбирает состав ревью и глубину проверки — и не выбирает их больше
никто.** Состав прогона постоянный: он один и тот же на всякой задаче
(`av-dev:code-review`, «Состав прогона»). Прежде состав считала метка `small` ·
`medium` · `large`, и тогда эта строка отвечала на живой вопрос «не задаёт ли её
тип»; метки нет, и вопрос снят вместе с ней. Правило «предписание процесса в теле
задачи снимается» типом не отменяется, а подтверждается: он описывает работу, а
не то, как её проверять. **Стадия проекта состава тоже не выбирает**: изменение
на стройке ничем не проще того же изменения на доработке.
**Одно исполнителю тип всё же говорит — каким сценарием работу вести, и то не
один.** Скилл `av-dev:code-resolve` выбирает сценарий связкой из двух
признаков: тип **предлагает** (`chore` — обслуживание, `research` — разведка),
а подтверждает его предмет работы — есть ли что менять в спеках. Признаки
разошлись — работа останавливается, и тип меняется здесь, командой `edit --type`,
а не переклеивается исполнителем по ходу. Состава ревью это по-прежнему не
задаёт: он постоянный, а на прогоне без change его называет сам сценарий.
## Как написана задача
Три требования к тексту. Первое — про заголовок, два остальных про то, чтобы
задачу можно было **оценить, не открывая код**.
**Заголовок отвечает на вопрос своего типа.** Вопросов два, поэтому и форм две:
| Тип | Отвечает на | Пример |
| --- | --- | --- |
| ✨ `feature`, 🐞 `fix`, 🧹 `chore` | что нужно сделать | Печатать поле одним куском кода |
| 🔬 `research` | о чём разведка | Подсказка следующего хода |
Задача — **глаголом в неопределённой форме**, перед ним допускается «не»: «Не
отбрасывать молча лишние символы в ходе», а не «Лишние символы молча
отбрасываются». Описательный заголовок называет **состояние**, а из состояния не
видно, чего от работы ждут: «Ничья объявляется, пока клетки есть» одинаково
читается и как жалоба, и как задание, — и в списке, где решают «брать или не
брать», это разные вещи. `research` формы действия не несёт **намеренно**: её
исход знание, а что делать — ещё неизвестно, и заголовок-действие обещал бы
решённость, которой нет.
`check` считает заголовки не в форме действия и печатает **число** в блоке
здоровья, не замечанием на файл: проверка эвристическая (первое слово на
`-ть`/`-ти`/`-чь`), а беклог, заведённый до правила, не переоформляют «заодно».
Годность формулировки — не машине: её смотрит
[агент вычитки](#вычитка-два-прохода-а-не-один).
**Функции и границы, а не намерения.** Задача называет, что система начнёт
делать, и какие границы это трогает: эндпоинт или команду, таблицу и миграцию,
формат на диске, публичный тип пакета, внешний сервис. Перечень живёт разделом
«Затрагивает» (форма — [references/task-format.md](references/task-format.md)) и
требуется к взятию в работу. Без него задача оценивается по объёму текста, а не
по объёму поверхности, — и оценка систематически занижена ровно там, где текст
короткий, а границ много. Названы **границы**, а не то, как они изменятся: план
реализации живёт в предложении об изменении, а не в задаче.
**Предметно, но без усложнения.** Текст задачи читает человек, который решает,
брать её или нет, и делает это по строке индекса и одному экрану тела.
Язык — общий для всех проектных текстов, и дом у него один:
[shared/language.md](../../shared/language.md) — информационный стиль,
применённый к задачам и документам канона; там же таблицы англицизмов и жаргона
и то, что из стиля отброшено намеренно. Задаче он даёт четыре требования,
которые нарушаются чаще прочих:
- **глагол вместо отглагольного существительного**: «обработчик не проверяет
владельца», а не «проверка владельца не осуществляется»;
- **факт вместо оценки**: «время ответа доходит до 800 мс», а не «работает
медленно». Оценка без факта рядом — настроение, а не сведение;
- **англицизм с живым русским аналогом заменяется**: не «зафиксить флоу», а
«починить порядок доставки». Имя вещи не переводится: слаг, команда, тип в
коде, `API`;
- **термин не из документов проекта вводится одной строкой** или не
употребляется. Свой словарь у задачи — самый дешёвый способ сделать беклог
нечитаемым для того, кто вернётся к нему через квартал.
И одно требование, которое есть только у задачи: **сложность формулировки — не
признак сложности работы.** Задачу, которую не удаётся сказать просто, чаще
всего не удаётся и оценить: это либо две задачи, либо сырьё.
Эти правила — про **язык**, а не про объём: короткая задача без границ хуже
длинной с ними.
## Инструмент (`tasks.py`)
Пусть `tk="$CLAUDE_PLUGIN_ROOT/skills/task-track/scripts/tasks.py"`, а `D`
`tasks` от корня проекта. `--dir` стоит в примерах намеренно: вызов из
подкаталога — обычное дело.
```
python3 $tk check --dir D # согласованность индекса + здоровье
python3 $tk check --dir D --fix # + починить дрейф (тип, эмодзи, место, заголовок, дубли, «зачем», форма меты)
python3 $tk list --dir D [--stale] [--section S] [--type T] [--tag a,b] [--raw] [--questions]
python3 $tk add --dir D --slug S --title T --type feature|fix|chore|research [--section S] [--why «зачем»] [--tag a,b]
python3 $tk edit S --dir D [--title T] [--why «зачем»] [--type T] [--add-tag a,b] [--rm-tag c]
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 ready S… --dir D # схема типа выполнена — можно брать в работу
python3 $tk stage --dir D # показать стадию
python3 $tk stage support --dir D [--sections …] # сменить стадию: секции и смысл порядка
python3 $tk init --dir D --stage build|support [--sections …] [--items …] …
python3 $tk adopt scan --from … --stage S | apply --plan … # разовая адаптация, references/adopt.md
```
**Копия.** Дом словаря — `shared/axes.md` в репозитории плагина: словарь общий
для всех скриптов, и ни один скилл им не владеет. Правится дом, а не этот файл.
<!-- копия: коды-выхода из av-dev/shared/axes.md -->
**Коды выхода — общий словарь всех скриптов `av-dev`. Ветвись на коде, а не на
тексте вывода.**
| Код | Что случилось |
| --- | --- |
| 0 | сошлось |
| 1 | дрейф: рабочая ситуация, чинится |
| 2 | ошибка употребления: аргументы или нарушенное правило |
| 3 | окружение: не тот каталог, битый конфиг, нет инструмента |
| 4 | внутренний сбой — дефект скрипта, доложить |
**Различать 1 и 3 обязательно.** «Дрейф» — рабочая ситуация, и чинится она
правкой предмета; «окружение» — нерабочая, и повтор той же командой не поможет.
Одинаковая реакция на них неверна в обоих случаях.
<!-- /копия: коды-выхода -->
Здесь это значит: код 1 приходит **только от `check`** — найден дрейф индекса и
файлов, чинится `check --fix`, остаток разбирается руками. Код 3 — каталог не
найден, конфиг битый или мимо диска: чинится путём или `.av-dev.toml` в корне.
Тип — английское ключевое слово `feature` / `fix` / `chore` / `research` (как и
прочие токены команд), у `add` **обязательное**: без него неизвестно, какой
шаблон тела класть. Стадия — такое же слово, `build` / `support`, и у `init` она
обязательна по той же причине: без неё неизвестно, что писать в шапке беклога и
сколько заводить секций. Текст задачи при этом русский, а эмодзи в заголовке
ставит скрипт.
**Мутации правят файл и индекс заодно** — руками строку индекса или мету
не пиши, зови `add`/`edit`/`move`/`close`/`reopen`. Смена заголовка, «зачем», типа
и **тегов** — это `edit`: он держит H1 (вместе с эмодзи), мету и индекс
согласованными. Снятие тега — `--rm-tag` (после ответа на вопрос снимается
`question`), смена типа — `--type`; оба заменяют прежнее значение, а не
добавляют второе.
**Секцию меняет только `move`, и он пишет причину**: смена полки без причины и
есть тот дрейф, который потом никто не объяснит.
**`move --after <слаг>` и `move --first` — это и есть расстановка порядка.**
Порядок строк в секции значим (правило 4), и двигают его только этой командой:
руками поправленная строка не оставляет причины, а причина здесь и есть половина
решения. Что именно этот порядок значит, говорит стадия: на стройке `--after`
называет зависимость, на доработке — приоритет.
Тело задачи скрипт не трогает:
`add` кладёт заголовок, мета-блок и шаблон с подсказками, тело дописываешь
редактором (пока плейсхолдер на месте, `check` напоминает).
`check` — единственный судья согласованности; что именно он ловит, скажет его
вывод, здесь не пересказываем. Гоняй его **в начале сессии** и **после каждой
правки**, даже если правил мутациями: дрейф мог накопиться раньше. Накопившееся
чини `check --fix` — он детерминированно правит то, где истина однозначна (тип в
своё поле, эмодзи заголовка, имя поля места, секция, заголовок, дубли, «зачем» из
индекса в файл, старая форма меты, снятые теги упразднённых целей, сырьё
в конец секции), а неоднозначное (нечего восстанавливать, **тип, которого
неоткуда взять**, запись типа `goal`) печатает отдельной пометкой
`НЕОДНОЗНАЧНО` — это тебе, и это идёт строкой доклада. **Ссылка на исчезнувший
файл в пометку не попадает:** `--fix` её просто не трогает, и она остаётся
`ОШИБКА` обычного `check` — то есть видна, но в докладе её надо назвать отдельно.
`--fix` правит **и файлы** — там, где источник ровно один и выбирать не из чего:
тип переезжает из прежнего дома (тег `kind:`, префикс `[goal]`/`[idea]`) в поле
меты, заголовок получает эмодзи, поле места зовётся «Категория», «зачем»,
оставшееся только в индексе, переезжает в мету, теги `goal:` и `decomposed`
снимаются. Каждый случай печатается поимённо.
**Ни стадию, ни состав секций `--fix` не трогает.** Стадию он подставить не
может — это решение человека; секции стройки не сливает — в каком порядке пойдут
строки слитых полок, знает тоже только человек, а порядок здесь и есть
содержание.
**Тип, который не выводится ниоткуда, `--fix` не угадывает.** `feature` от
`chore` машина не отличает, и подставленное наугад значение врало бы ровно там,
где по нему принимают решение. Такие записи идут в `НЕОДНОЗНАЧНО`, и тип им
проставляет человек — `edit <слаг> --type …`.
**Что механизировано, а что нет.** Схему типа проверяет `ready` на входе в
работу — там, где по ней принимают решение; `check` поимённо о ней не говорит, а
считает: строка здоровья **«схема типа не выполнена: N из M»** называет число и
первые слаги, строка **«готово к взятию»** — сколько задач беклога пройдут
`ready` целиком (схема плюс отсутствие открытого вопроса). Это две
разные строки, и совпадение их чисел — совпадение. У каждой части своя глубина:
- **тип** — жёстко: назван и из закрытого словаря;
- **критерии приёмки** (`feature`, `fix`, `chore`) — число пунктов жёстко
(меньше двух отказ, больше пяти замечание), наличие оракула **эвристикой** по
слову «оракул» в пункте;
- **прочие разделы схемы** (`Затрагивает`, `Воспроизведение`, `Вопрос`,
`Куда ляжет ответ`) — только **наличие непустого**. Содержимое
машине не видно: границу, которую забыли назвать, она от отсутствующей не
отличает, а шаги, по которым ничего не воспроизводится, — от годных.
Настоящий оракул от слова «оракул» машина тоже не отличает, поэтому эвристика
даёт только замечание, и в докладе это называется как есть: «проверено наличие
разделов своего типа и число критериев, годность оракулов и полнота границ —
глазами».
Формат записи, меты, слага, индекса и `REJECTED.md`
[references/task-format.md](references/task-format.md); там же тест «готова к
взятию». Схема и алгоритм каждого типа — по файлу на тип:
[feature](references/task-feature.md) ·
[fix](references/task-fix.md) · [chore](references/task-chore.md) ·
[research](references/task-research.md).
## Версия раскладки
Формат каталога задач меняется, и проект должен знать, к какой версии он
приведён. Число живёт ключом `version` в `.av-dev.toml` в корне репозитория,
журнал версий — [журнал скилла `canon`](../canon/references/changelog.md),
сверяет их `tasks.py check`: отстало — строка расхождения, ушло вперёд — устарел
плагин. Обратной совместимости нет: есть «приведён» и «не приведён».
**Версия одна на всю раскладку — и на документы, и на задачи.** Своя у каталога
задач была, пока плагинов было три и ставились они порознь: проект мог взять
учёт работ без канона документов, и общее число оказалось бы домом, которого у
половины проектов нет. Плагин один — довод ушёл, а два числа оставляли бы
вопрос, по какому журналу повышать.
**Повышает проект скилл `av-dev:canon`, операция `upgrade`** — он идёт по
журналу, двигает число и зовёт этот скилл там, где запись касается задач. Здесь
повышения нет намеренно: две операции, двигающие одно число, разъезжаются на
первом же проекте, где прошла только одна из них.
## Сценарии
### Завести запись из диалога
0. **Посмотри стадию**`stage`. От неё зависят шаг 1 и место новой строки: на
доработке беклог пополняют по одной и с фильтром, на стройке пишут планом.
1. **Фильтр — на доработке.** Делаем прямо сейчас — не заводим. Не пожалеем о
потере — не заводим. Родилось три кандидата — покажи их и спроси, какие
заводить: молча заведённая пачка и есть тот самый отказ из правила 1.
**На стройке фильтра нет**: план пишется вперёд целиком, и «этого мы сейчас
не делаем» — не довод против шага, а описание всякого шага, кроме первого.
2. **Дедуп.** `list` плюс поиск по слагам, полю «зачем» и телам (`grep -ril`),
**включая `REJECTED.md`**. Нашлось среди живых — **дописываем в существующий
файл**, а не заводим соседний. Нашлось в `REJECTED.md` — покажи пользователю
ту строку и что изменилось с момента отказа (`add` предупредит и сам, но
молча заводить нельзя). Две задачи об одном — самая дорогая находка
переоценки.
3. **Тип**`--type` обязателен, и он же первое содержательное решение:
- снаружи появляется то, чего не было → `feature`;
- поведение расходится с заявленным и **воспроизводится**`fix`
(не воспроизводится → `research`);
- обслуживание, наблюдаемое поведение не меняется → `chore`;
- исход — знание, а не изменение системы → `research`.
Не подходит ни один — в записи их два, разбирай. Не проходит тест готовности
(см. task-format) — это **сырьё**: `--type research`, раздел «Вопрос» пока
пуст, место в конце секции. Не делается одним заходом — дроби на шаги
помельче и ставь их в списке подряд.
4. **Место в списке.** `add` кладёт строку в конец секции всегда. На стройке это
почти наверняка не то место: порядок там зависимость, и новый шаг чаще всего
встаёт в середину — `move <слаг> --after <слаг>`. На доработке конец списка
законен: место в очереди назначает груминг, а не заведение.
5. `add …`, затем допиши тело редактором **по схеме своего типа** — шаблон её
уже разложил, устав типа объясняет каждый раздел. «Зачем» отвечает «зачем
нужна эта задача» — состояние, остаток, боль, — а не пересказывает первый
абзац, и пишется **для человека**: не «канонизация внутри транзакции», а
«тело 40 МиБ держит блокировку 5 секунд, соседние доставки уходят в отказ».
6. `check`.
### Разобрать находки аудита или ревью
Ревью и аудиты — тоже источник задач, но с опасностью, зеркальной диалогу: не
пять файлов из одной мысли, а сорок файлов из сорока сырых находок. Защита та
же, что в самом ревью: кластеризация по причине, дедуп против живых и
`REJECTED.md`, находка без свидетельства → сырьё (`research`), а не задача, и карта кластеров
пользователю до создания файлов. **Стадию смотри и здесь**, тем же нулевым
шагом: от неё зависит, куда ляжет тяжёлая находка — наверх очереди или после
своей зависимости. Порядок и отображение серьёзности —
[references/from-review.md](references/from-review.md).
### Прийти в репозиторий, где задачи уже как-то ведутся
Разовая операция: вывести каталог задач из старой раскладки беклога, `TODO.md`,
заметок или списка шагов плана — [references/adopt.md](references/adopt.md).
Сюда же относится переименование транслитных слагов в английские: оно делается
**одним проходом вместе с починкой перекрёстных ссылок**, а не по одному слагу.
Если переводить надо не только задачи, а весь `docs/` — это скилл
`av-dev:canon`, и он зовёт этот сценарий сам на своём шаге.
### Пересмотр плана стройки
Операция стадии `build`, и на доработке её нет: там переоценка идёт порциями и
называется грумингом. **Повод один — сменился замысел**, а не «давно не
смотрели»: план стройки протухает не по частям, а целиком, потому что порядок в
нём — зависимость, и одна изменившаяся посылка переставляет всё, что ниже.
Порционный разбор здесь вреден, и это не вкус: вынуть пять шагов из середины
списка, порядок которого и есть его содержание, — значит получить план, про
который никто уже не скажет, почему он такой.
1. **Назови, что изменилось в замысле.** Одной фразой, и она уедет причиной в
каждое движение. Не находится — значит повода нет, и пересмотр не нужен.
2. **Прочитай список целиком**, сверху вниз, и по каждой строке ответь одно из
трёх: остаётся как есть, переезжает (`move --after` с причиной), уходит
(`close --reason`). Дописанное новое встаёт туда, куда велит зависимость, а
не в конец.
3. **Покажи человеку весь новый список**, а не отдельные решения: план читается
только целиком. `AskUserQuestion` с готовым порядком и доводом на каждое
движение.
4. `check` и доклад: сколько строк тронуто из скольких, что ушло и почему.
**Границу с грумингом держи твёрдо.** Если хочется пересмотреть план «потому что
накопилось» — это не пересмотр, а признак того, что стройка кончилась: беклог
перестал быть планом и стал очередью. Проверь `stage`.
### Декомпозиция и штурм сырья
[references/split.md](references/split.md). Обе операции превращают одну запись в
несколько, и у обеих есть проверяемый тест: части должны **мерджиться порознь** и
**каждая давать видимую пользу**, а у штурма исход «выкинуть» — полноправный.
Там же **шов**: где резать, когда допустимых мест несколько. Коротко — по
границе, где **меняется род работы**; и резать пореже, потому что костяк ревью
разрез удваивает **всегда** — состав прогона постоянный и от размера половин не
зависит. Выигрыш даёт не проверка, а то, что половина доводится и мерджится сама.
### Вычитка: два прохода, а не один
Записи судит **не тот агент, который их написал**: самопроверка текста слабее
всего ровно там, где формулировка казалась удачной при написании. Проходов два,
и они разные по природе:
| Проход | Что смотрит | Над чем работает |
| --- | --- | --- |
| `task-form` | заголовок по типу, «зачем» вместо пересказа, границы вместо замысла, годность оракулов, предписание процесса | только `items/` |
| `task-wording` | залог и отглагольные, оценка без факта, стоп-слова, англицизмы, жаргон, неизвестный термин, транслит в слаге | `items/` и строки индекса; документы проекта — только как словарь |
Разделены они не по охвату, а **по глубине**. Язык проверяется по словам и
фразам, поштучно; форма записи требует понять, что задача делает. Слитый проход
одну половину делает дорогой, а вторую — поверхностной.
Модель у обоих одна, `sonnet`, и это не отменяет разреза. Оба судят по
**записанному правилу** — шесть пунктов формы против правил языка, — а их находка
приходит готовой формулировкой, которую читает и отклоняет человек, а не молча
реализует оркестратор. Ошибка здесь стоит строки чтения, и платить за неё верхней
моделью не за что.
Каждый устав отказывается от чужой половины прямо: увиденное не по своей части
идёт **строкой в границах покрытия**, а не находкой. Две проверки одного места
расходятся и начинают спорить, и разнимать их потом дороже, чем не сводить.
**Порядок — сперва `task-form`.** Его находки меняют решение «брать или не
брать», а язык — только цену чтения; и переписанный заголовок бессмысленно
вычитывать до того, как он переписан.
Зовутся они **пачкой, а не на каждую запись**: после заведения нескольких задач,
после разбора находок ревью, после того как чужая работа уточнила записи (так
делает разведка в `av-dev:code-resolve`), и на переоценке. Передаётся список файлов и — если
есть — паспорт, архитектура и конвенции проекта: по ним отличается неизвестный
термин от известного.
Ни один из них ничего не правит. Оба возвращают готовые формулировки, и их
подставляет скилл: заголовок — `edit <слаг> --title …`, «зачем» —
`edit <слаг> --why …`, остальное редактором. **Заголовок и «зачем» — это то, по
чему задачу выбирают, поэтому менять их молча нельзя**: покажи предложенное
пользователю вместе с тем, что было. Правки в теле (границы, критерии, язык)
применяются сразу.
Всё, что ловит `tasks.py check`, оба не трогают намеренно.
### Гигиена полей
Правится по ходу любой операции, которая задачи касается (но не «заодно» по
всему беклогу):
- **протухшее «зачем»** — задача изменилась, а поле отвечает на старый вопрос;
особенно после ответа на вопрос задачи: «Решено: …» на «почему это лежит в
беклоге» уже не отвечает. Переписывается `edit <slug> --why …` — он правит
мету файла и строку индекса заодно;
- **вопрос, застрявший в прозе** — вынимается в раздел «Вопросы» плюс тег
`question` (`edit --add-tag question`), иначе он не виден ни `list
--questions`, ни правилу «задача с открытым вопросом в работу не берётся»;
- **тег, который некому снять**`question` после ответа снимается `edit
--rm-tag question` вместе с записью ответа в тело **и опустошением раздела
«Вопросы»**: судит раздел, а не тег (`references/task-format.md`);
- **свойство репозитория в рамках** — номер миграции, хеш, версия зависимости:
в лежалой задаче протухает молча и становится ложной рамкой. Снимается;
снимок берётся при постановке, а не при заведении;
- **предписание процесса в теле** — «прогнать глубоким ревью», «взять такой-то
агент», «этой задаче хватит короткой проверки»: это второй дом для правила
выбора и путь понизить требования решением, принятым до проектирования.
Снимается;
- **тип, разошедшийся с задачей** — задача заводилась починкой, а после разбора
оказалось, что поведение никогда и не было заявлено: это `feature`, а не `fix`.
Правится `edit <slug> --type …`; тип, оставшийся от прошлой формулировки, врёт
ровно там, где по нему отбирают, **и требует не тех разделов**: у брошенного
`fix` останется «Воспроизведение», которого нечем заполнить;
- **сырьё, у которого появился вопрос** — разведка обросла формулировкой, но
раздел «Вопрос» так и пуст: она числится сырьём и в работу не берётся.
Записывается вопрос, и `check --fix` поднимает строку из конца категории;
- **границы, названные вместо реализации** — «переписать хранилище на новый
драйвер» в разделе «Затрагивает» это не граница, а замысел. Границы —
`таблица points и её миграция`, `эндпоинт POST /ingest`, `формат отпечатка на
диске`. Переписывается перечнем;
- **англицизм и термин из ниоткуда** — правится по ходу той же операции, что
касается задачи (см. «Как написана задача»). Именно по ходу: беклог не
переписывают ради языка.
## Переносимость
Скилл независим от **языка программирования, сборки, CI и трекера**: он ничего
не знает ни про Go, ни про npm, ни про конкретный багтрекер — задачи для него
просто каталог markdown. Текст задач — русский (язык документации проекта);
зашита только латиница слага. OpenSpec ему тоже не нужен.
- **Каталог задач — `tasks/` в корне, жёстко**, и `--dir` передаётся явно всегда:
раскладка канона одинакова во всех проектах, и искать больше нечего. Каталога
нет — код 3 и вопрос человеку; `init` заводит его **только** когда проект
действительно новый, а перевод чужой раскладки делает `av-dev:canon`.
У скрипта поиск вверх по дереву ещё жив — он для непереведённых проектов, и
полагаться на него скилл не должен: молча найденный чужой каталог это дрейф.
- **Версия и настройки живут в `.av-dev.toml` в корне репозитория** — версия
ключом `version`, настройки каталога секцией `[tasks]`: `dir` — где каталог
лежит, `stage` — стадия проекта, плюс **имена** файлов и заголовков, и
последние только если отличаются от умолчания. Неизвестный ключ в секции — код
3 на любой команде, так что лишнее слово останавливает работу с задачами
целиком.
Дом в корне, а не внутри каталога задач, по двум причинам: настройка, лежащая
внутри настраиваемого каталога, не смогла бы сказать, **где он**; и версия
одна на весь плагин, а корень есть и у проекта без `docs/`. Прежние
`<каталог задач>/.tasks.json` и `docs/.docs.json` не читаются — увидев их,
скрипт говорит «прежняя раскладка» и зовёт `upgrade`.
- **Секции беклога** берутся из заголовков `##` индекса как есть; их названия —
дело проекта (умолчание `План` на стройке, `Ядро` / `Инфра` на доработке), а
количество ограничено стадией: на стройке секция одна. **В конфиге секций
нет** — второй список разошёлся бы с заголовками молча.
### Вызов из другого плагина
`$CLAUDE_PLUGIN_ROOT` раскрывается **только внутри своего плагина**: конвейер
задачи, конвейер ревью и любой другой чужой контекст до `tasks.py` по этой
переменной не дотянутся. Мост — **вызов скилла через пространство имён**, а не
путь:
> Чужой контекст зовёт `Skill av-dev:task-track` и называет, что нужно сделать
> («закрой задачу `<слаг>`, реализована»). Скилл разрешает свой
> `$CLAUDE_PLUGIN_ROOT` сам. Путь наружу не выносится вовсе.
Каталога задач в проекте нет — вызывающий **не выдумывает путь и не правит
индекс руками**, а сообщает в докладе, что учёт остаётся за владельцем. Скилл
при этом разрешится: он в том же плагине, что и вызывающий.
## Слоты проекта
Почти всё, что скиллу нужно знать о проекте, отвечает канон структурой: куда
переезжает суть реализованной задачи — `openspec/specs`, `adr/`, архив change;
какие в проекте оракулы — семантика гейта в `CLAUDE.md`. Отдельными слотами
остаётся то, чего из раскладки не вывести. **Проект дописывает в `CLAUDE.md`**:
1. **Что такое «сделана»** — чем задача выполняется (конвейер проекта) и что
входит в его определение сделанного. Скилл требует лишь **форму**: конвейер
проекта пройден + критерии приёмки проверены поимённо.
2. **Что считается необратимым** и потому спрашивается у человека всегда
(деплой, выкладка наружу, удаление или перезапись данных).
Ни того ни другого скилл не угадывает: не нашёл — спрашивает пользователя, а не
подставляет умолчание.
## Общее для всех сценариев
- **Развилки — пользователю.** Через `AskUserQuestion`, с уже сформулированным
предварительным суждением (**рекомендация — первым вариантом**). Что выкинуть,
какая рамка разведки верна, пора ли менять стадию — решение пользователя. Слаг
и формулировка — механика, делаем сами. **Порядок строк механикой не
считается** ни на одной стадии: на стройке он зависимость, на доработке
приоритет, и оба называет человек.
- **Не больше трёх вопросов за раз.** Пачка длиннее трёх тяжела для ответа;
решений больше — веди **несколько итераций** диалога по ≤3, а не один
перегруженный запрос. Между итерациями применяй уже решённое.
- **Границы покрытия в отчёте.** Любая сессия разбора, штурма или заведения записей
заканчивается строкой «просмотрено N из M, не трогали — …». Отчёт без неё
сообщает «беклог разобран», не сообщая, какая его часть осталась нетронутой.
- **Ничего не удаляем молча.** Файл исчезает только через `close``--reason`
(ушла без реализации) или `--implemented` (реализована). Прямого `rm` нет.
- **Слаги английские**, kebab-case, не транслит: `tie-break-equal-completeness`,
а не `taj-brejk-pri-ravnoj-polnote`. Заголовки, тела и «зачем» — русские.
## Чего этот скилл не делает
Не пишет код, не заводит спеки и предложения об изменении, не берёт задачу в
работу — этим занимается конвейер проекта. **Не ведёт очередь:** что делать
следующим и что перестало быть важным — скилл `task-groom`, а этот даёт ему операции.
Не решает за пользователя, что важно. Не
переоформляет существующие задачи «заодно»: правится то, чего касается операция.
@@ -1,24 +1,24 @@
# Адаптация каталога задач
Проект, где задачи уже как-то ведутся, и из имеющегося материала **выводится**
заполненный каталог задач: цели, задачи, кладбище, индексы. Операция разовая —
после неё проект живёт скиллами `tasks` и `session`.
заполненный каталог задач: задачи, кладбище, индекс. Операция разовая —
после неё проект живёт скиллами `task-track` и `task-groom`.
**Это часть приведения проекта к канону.** Раскладку `docs/` целиком ведёт скилл
`av-dev-pm:canon`; он же зовёт этот сценарий на шаге «каталог задач», потому что
форматом задач владеет `tasks`, а не `canon`. Отдельно сценарий вызывается,
`av-dev:canon`; он же зовёт этот сценарий на шаге «каталог задач», потому что
форматом задач владеет `task-track`, а не `canon`. Отдельно сценарий вызывается,
когда переводить надо **только** задачи.
Вход какой угодно: старая раскладка `av-dev-backlog` (индекс `README.md`,
кладбище `CLOSED.md`, приоритеты секциями, транслитные слаги, файлы рядом с
индексом), `TODO.md`, россыпь заметок, раздел «планы» в `README.md`, список
шагов роадмапа проекта.
шагов плана проекта.
## Три правила, из которых всё следует
1. **Сперва карта, потом файлы.** Человеку показывается, что найдено, как
разложилось по целям и **что не разложилось**, — и только после подтверждения
пишется хоть один файл. Это то же правило, что у интейка находок ревью:
1. **Сперва карта, потом файлы.** Человеку показывается, что найдено, в каком
порядке разложилось и **что не разложилось**, — и только после подтверждения
пишется хоть один файл. Это то же правило, что у заведения задач из ревью:
массовое заведение записей без подтверждения — самый дорогой отказ, потому
что разгребает его потом переоценка.
2. **Ничего не терять.** Исходный текст переезжает в тело, «зачем» и причина
@@ -35,10 +35,10 @@
«машина умеет / не умеет»:
```
tk="$CLAUDE_PLUGIN_ROOT/skills/tasks/scripts/tasks.py"
tk="$CLAUDE_PLUGIN_ROOT/skills/task-track/scripts/tasks.py"
python3 $tk adopt scan --from docs/backlog docs/plan.md TODO.md \
--target docs/tasks --out tasks-adopt-plan.json # только чтение
--stage build --target tasks --out tasks-adopt-plan.json # только чтение
python3 $tk adopt apply --plan tasks-adopt-plan.json \
--refs docs openspec CLAUDE.md README.md # запись
```
@@ -53,71 +53,85 @@ python3 $tk adopt apply --plan tasks-adopt-plan.json \
- **английские слаги.** Перевести `taj-brejk-pri-ravnoj-polnote` в
`tie-break-equal-completeness` может только тот, кто понимает смысл. `scan`
честно говорит: проверить надо **все** слаги, признаки транслита — эвристика;
- **цели.** Шаги роадмапа — готовые цели из **«порядка»** (очередь и обоснование у
них уже есть); тематические скопления задач — **«темы»** («прочность слияния»,
«журнал и пересборка»). Предлагаешь ты, назначает человек;
- **стадия.** `--stage` называет, чем этот беклог будет: планом стройки или
очередью правок. Машине это не выводится — она видит список пунктов, а не то,
построено приложение или нет;
- **порядок.** Нумерованные шаги источника `scan` сохраняет по номерам, прочие
ставит следом. Дальше порядок — твоё суждение и подтверждение человека: на
стройке это зависимость, на доработке важность;
- **что вообще не задача.** Обоснование порядка шагов, абзац прозой, заголовок
раздела — это не пункты беклога, и они уходят в «не разложилось» с причиной.
## Порядок
1. **Осмотрись.** Где лежат задачи, роадмап, заметки. Каталог задач по канону —
всегда `docs/tasks`. Секции беклога (`--sections`) — по умолчанию
`Ядро,Инфра`; если у проекта деление другое по существу, оно называется
здесь, а не подгоняется под умолчание, и становится **заголовками `##`
индекса** — их единственным домом. В `docs/.pm.json` секции не пишутся.
1. **Осмотрись и назови стадию.** Где лежат задачи, план, заметки; построено
приложение или строится. Каталог задач по канону — всегда `tasks`. Секции
беклога (`--sections`) — на стройке ровно одна (умолчание `План`), на
доработке сколько нужно (умолчание `Ядро,Инфра`); если у проекта деление
другое по существу, оно называется здесь, а не подгоняется под умолчание, и
становится **заголовками `##` индекса** — их единственным домом. В
`.av-dev.toml` секции не пишутся: там версия, стадия и имена частей, а второй
список секций разошёлся бы с заголовками молча.
2. **`adopt scan`** по всем источникам разом. Один прогон, одна карта: два
прохода дадут два несогласованных состояния.
3. **Заполни карту**: `slug` (английский), `section`, `goal` у каждой записи;
список `goals` — из шагов роадмапа и из тем. Закрытый шаг целью не
заводится. Пустой `goal` — законный исход только у идеи.
3. **Заполни карту**: `slug` (английский), `type` и `section` у каждой записи, и
**порядок `items`** — он уедет в индекс как есть. Пункт, помеченный
закрытым, не переносится вовсе.
4. **Покажи человеку карту** через `AskUserQuestion`, ≤3 вопроса за итерацию,
рекомендация первым вариантом. Показывается: сколько записей, предлагаемые
цели (порядок и темы) с обоснованием, спорные отнесения, список «не
разложилось». Массовые механические решения (слаги, порядок строк) не
выносятся — это механика.
рекомендация первым вариантом. Показывается: сколько записей, предлагаемый
порядок с обоснованием, спорные отнесения, список «не разложилось». Слаги не
выносятся — это механика; **порядок выносится всегда**, потому что механикой
он не является ни на одной стадии.
5. **`adopt apply`.** `--refs` перечисляет **всё**, где могут стоять ссылки на
слаги: документация, архив изменений, `CLAUDE.md`, `README.md`. Скрипт
посчитает и покажет, сколько ссылок поправлено и по каким слагам.
6. **`tasks.py check`** и доклад.
`apply` отказывается писать поверх живого каталога и проверяет карту целиком
**до** первой записи: неверная секция, дубль слага, цель, которой нет в карте —
всё это отказ до того, как на диске появился хотя бы один файл.
**до** первой записи: неверная секция, дубль слага, неназванный тип, две секции
при стадии `build`всё это отказ до того, как на диске появился хотя бы один
файл.
## Переходное состояние — объявляется, а не заминается
Сразу после адаптации задачи в большинстве своём **не готовы к взятию**: у них
нет критериев приёмки, а у части может не быть цели. Это нормально, но обязано
быть названо, иначе следующий агент примет пустой беклог за поломку.
нет критериев приёмки. Это нормально, но обязано быть названо, иначе следующий
агент примет пустой беклог за поломку.
`apply` печатает состояние по факту: сколько задач без цели (это **ошибки**
`check`) и сколько без критериев (`check` их ошибкой не считает, но `sprint
take` такую задачу не возьмёт). Закрывается это **порциями переоценки** — шаг 3
скилла `session`, 5–8 задач за порцию: проставить цели, превратить «готово,
когда» в критерии с оракулами, вынуть вопросы из прозы в раздел «Вопросы».
`apply` печатает состояние по факту: сколько задач не собрало разделы своего типа
(для `check` это не ошибка, а строка здоровья, но `ready` такую задачу не
пропустит). Закрывается это **порциями по 58 задач**: превратить «готово,
когда» в критерии с оракулами, вынуть вопросы из прозы в раздел «Вопросы». На
доработке это груминг (скилл `av-dev:task-groom`), на стройке — гигиена полей
этого скилла: груминга там нет.
Готовность к первому спринту — не «`check` зелёный», а «есть 2–5 критериев хотя
бы у набора под одну цель».
**Порядок строк проверяется глазами отдельно.** На стройке он выведен из
нумерации источника, и там, где её не было, он случаен. На доработке машина
важности не знает вовсе — очередь расставляется первым же грумингом.
Готовность к первой задаче — не «`check` зелёный», а «`ready` пропускает хотя бы
верхние строки очереди».
## Чего адаптация не делает
- **Не удаляет источники.** Старый каталог остаётся на месте: сверить и убрать —
дело человека, удалять чужое молча нельзя. В доклад идёт готовая команда.
- **Не переписывает подписи ссылок.** `[docs/backlog](docs/tasks/BACKLOG.md)`
- **Не переписывает подписи ссылок.** `[docs/backlog](tasks/BACKLOG.md)`
цель поправлена, текст остался; это правится глазами, и таких мест немного.
- **Не сочиняет критерии приёмки и не придумывает цели**, которых в материале
нет. Придуманная цель хуже отсутствующей: под неё соберут спринт.
- **Не сочиняет критерии приёмки** и не додумывает пользы, которой в материале
нет.
- **Не выводит стадию.** Список пунктов одинаково выглядит и планом стройки, и
очередью правок; отвечает `--stage`, а называет его человек.
- **Не трогает историю.** В коммитах старые слаги остаются, и это нормально.
## Доклад
- Источники и что в каждом распознано (раскладка, индекс, кладбище, секции).
- Сколько записей перенесено, сколько целей заведено (порядок / темы) и откуда
каждая выведена.
- Стадия и сколько записей перенесено; откуда взялся порядок (нумерация
источника или суждение).
- **Переименования**: сколько слагов, сколько ссылок поправлено и в скольких
файлах — числом, а не «поправлены ссылки».
- **Не разложилось**: поимённо, с причиной.
- Переходное состояние: сколько задач без цели, сколько без критериев, чем и за
сколько порций закрывается.
- Переходное состояние: сколько задач без критериев, чем и за сколько порций
закрывается.
- `tasks.py check` — результат строкой.
@@ -0,0 +1,147 @@
# Задачи из аудита и ревью
Ревью и аудиты — код-ревью, архитектурный проход, аудит безопасности, любой
разбор другим агентом — порождают находки, часть которых становится задачами.
Это отдельное **заведение записей** со своей опасностью, **зеркальной**
заведению из диалога. Операция зовётся по источнику, потому что источник и
задаёт опасность.
- Заведение из диалога грешит переполнением: из одной мысли рождается пять
файлов.
- Заведение из ревью грешит сваливанием: сорок сырых находок превращаются в сорок
файлов. Беклог раздувается, а следующая переоценка склеивает их обратно.
Защита от сваливания — та же, что в самом ревью: **кластеризация по причине, а
не файл-на-находку.** Если у ревью был триаж — половина работы уже сделана, бери
его выход. Если нет — триажируй сам, прежде чем заводить.
**Штатных отправителя два.** Первый — `av-dev:code-review` (и зовущий его
`av-dev:code-resolve`): задач он не заводит сам, а отдаёт отложенные находки
**списком урожая** — формулировка, оракул, откуда взялась — и хранит отчёт триажа
вместе с изменением. Второй — `av-dev:code-deep-review`, и он зовёт этот сценарий
напрямую, передавая согласованные с человеком находки дословно. Приходит и любой
другой разбор, вплоть до пересказа человеком; тогда триажа нет и шаг 1 порядка
делается руками.
## Находка агента — не задача
Мнение агента — **гипотеза, пока у неё нет свидетельства** (падающий тест,
воспроизводимый шаг, положение руководства). Согласие нескольких находок само по себе
достоверность не повышает: это один источник, высказавшийся несколько раз.
Отсюда фильтр входа, поверх обычного «не делаем сейчас + пожалеем о потере»:
- **Находка со свидетельством**, отложенная к исполнению → **задача**.
Свидетельство и последствие переносим в тело — это её «почему», то самое, что
переживает запись.
- **Находка без свидетельства / низкой уверенности****сырьё**: `research`, у
которого раздел «Вопрос» и есть недостающее свидетельство («при каких условиях
это воспроизводится»). Не `fix`: без `Воспроизведения` его в работу не
возьмут, и правильно — чинить нечего, пока непонятно, что ломается. Судьба
сырья — штурм, где либо найдётся подтверждение, либо оно уедет в
`REJECTED.md`.
- **Уже починено по ходу ревью****ничего**. Починенное не заводим.
- **Развилка, решённая при ревью** → ничего; решённая «потом» → задача с
вопросом в разделе «Вопросы» и тегом `question`.
## Порядок
1. **Возьми выход триажа, а не сырые находки.** Сырой отчёт — это симптомы до
дедупликации; в нём одна причина размазана по нескольким строкам.
2. **Кластеризуй по причине.** Пять находок об одном отсутствующем инварианте —
одна задача, а не пять. Класс мелочи (nits, косметика) — **один пакетный
файл** со списком пунктов, а не файл на каждую запятую.
3. **Дедуп против живых задач и `REJECTED.md`.** Аудит переоткрывает уже
заведённое и уже выкинутое. Нашлось среди живых — дописываем находку в
существующий файл. Нашлось в `REJECTED.md` — это сигнал: причина отказа могла
устареть, выноси пользователю, а не заводи молча заново.
4. **Проставь типы.** Большинство находок ревью это `fix` и `chore`. Находка,
оказавшаяся **новой возможностью** (`feature`), — отдельный случай: нашлось
поведение, которого никто не заказывал, и решение тут не «завести задачу», а
«заказать или убрать». Выноси такую пользователю отдельно от прочих.
5. **Покажи карту до создания файлов.** Кластер → задача / сырьё / строка в
пакетный файл / уже заведено / отброшено — пачкой через
`AskUserQuestion`. Это тот же барьер, что и «три кандидата» при заведении
из диалога: массовое заведение файлов без подтверждения — ровно тот отказ,
ради которого заведение из ревью и выделено. Дешёвая мелочь по явному согласию может
заводиться и без поштучного вопроса — но карта пользователю предъявляется
всё равно.
**Барьер снимается ровно в одном случае — когда его уже прошли.** В хвосте
задачи (`av-dev:code-resolve`, шаг 6; в обслуживании — шаг 5) человек одной
репликой сказал, что из урожая заводится, и третьего стопа у прогона не будет:
там карта идёт **строкой доклада**, а не вопросом. Признак читается буквально:
**список находок уже был показан человеку и получил ответ**. Не был — карта
предъявляется вопросом, и это обычный случай прямого вызова и вызова из
`av-dev:code-deep-review`, где находки разбирались по одной, а нарезка — нет.
6. **Заводи утверждённое** через `tasks.py add`, с тремя добавками:
- **тег партии**`--tag review-ГГГГ-ММ-ДД` (или `audit-<slug>`), чтобы весь
заход разбора поднимался одной командой `list --tag …`;
- **тип**`--type`, и он **не по умолчанию `fix`**: починкой считается
расхождение с заявленным поведением, а находка «этого свойства никто не
заказывал» — это `feature`, находка «не знаем, как поведёт себя драйвер» —
`research`. Тип, розданный оптом, врёт ровно там, где по нему потом
отбирают, **и требует не тех разделов**: каждому `fix` придётся заполнить
`Воспроизведение`, а у находки без свидетельства его нет;
- **откуда взялась — в теле**: кто нашёл, каким проходом, с каким свидетельством.
Без него через месяц не отличить проверенную находку от догадки.
7. `tasks.py check`.
## Куда девается серьёзность находки
Уровня серьёзности в записи нет — но **выкидывать её нельзя**: серьёзность
отображается **в позицию в очереди**, потому что приоритет и есть порядок строк
в беклоге (правило 4 [SKILL.md](../SKILL.md)). Отображается через довод, а не
напрямую: своей шкалы у заведения нет, доводы расстановки перечислены в
[скилле груминга](../../task-groom/SKILL.md#приоритет-как-его-расставляют), и
серьёзность попадает ровно в один из них.
**Всё это — про доработку.** На стройке порядок строк значит зависимость, и
`--first` там означает «ни от чего не зависит», а не «важнее всех»: находка,
поднятая наверх, встанет перед собственной зависимостью. Место находке на стройке
называет зависимость — `move --after <шаг, после которого её можно делать>`, — а
серьёзность идёт **причиной в мете** и разбирается ближайшим пересмотром плана
(`task-track`, «Пересмотр плана стройки»). Груминга там нет, и откладывать «до
него» некуда.
- **тяжёлая находка со свидетельством о сломанном сейчас** → задача
**первой строкой секции**: `move <слаг> --first
--reason «сломано сейчас: …»`. Это довод «что сломано сейчас» из перечня
груминга — единственный, который не требует сравнения с соседями по очереди,
потому что сломанное дорожает само. Позицию всё равно назначает человек, и
здесь он её уже назначил: верх очереди для такой находки предъявляется картой
шага 5, а не проставляется молча;
- **тяжёлая находка о риске, а не о поломке** (дорожает от ожидания,
разблокирует остальное) → в конец секции, а довод — причиной в мете
(`--reason`). Позицию назначит человек на ближайшем груминге, сравнив её с
верхом очереди; без записанного довода сравнивать он будет с нуля;
- **находка, которая не ждёт груминга вовсе** (необратимый ущерб, покраснела
проверка, которую проект назвал сломанным), — не заведение записи: это работа прямо
сейчас, а в беклог она падает, только если ждать всё-таки можно;
- **низкая уверенность или нет свидетельства** → сырьё (`research` с пустым
разделом «Вопрос»): его место в очереди производно от типа — конец секции;
- **мелочь** → строка в пакетный файл;
- **уже починено / развилка решена сейчас** → ничего.
Словарей серьёзности много, и отображать их механически не на что: при сомнении
— вопрос пользователю, а не догадка.
## Поимённая сверка
Заведение считается выполненным, только если **каждая** находка триажа получила
исход: слаг заведённой задачи, ссылку на существующую, строку пакетного файла
или запись «не заведена: причина». Нулевой урожай при непустом отчёте триажа
виден сразу — и это единственный способ отличить «находок не было» от «не стал
заводить». Список составляет не тот, кто отчитывается о заведении.
Границы покрытия отчёта — то, что ревью проверить **не смогло**, — не находки и
в задачи не идут: у них нет предмета. Их место в докладе, не в беклоге.
## Доклад
- Источник (какое ревью/аудит, сколько находок на входе).
- Свёрнуто в задачи: N кластеров из M находок, со слагами и тегом партии.
- Что не заведено и почему: починено инлайн, уже заведено, стало сырьём, ушло в
`REJECTED.md`.
- Поимённая сверка: находок на входе N, исход есть у N.
- `tasks.py check`.
@@ -0,0 +1,112 @@
# Декомпозиция и мозговой штурм
Обе операции превращают одну запись в несколько (или в ноль). Разница во входе:
декомпозиция дробит **слишком крупную задачу**, штурм прорабатывает **идею**,
которая ещё не задача.
## Тест декомпозиции
Задачу можно дробить, только если части удовлетворяют **обоим** условиям:
1. **Каждая мерджится сама по себе.** Часть, после которой дерево не собирается
или поведение сломано до прихода соседней, — не часть, а половина.
2. **Каждая — самостоятельный шаг.** Часть, осмысленная только в комплекте с
другой, — не задача. Проверяй тестом «готова к взятию» (task-format): свои
критерии приёмки у неё есть или нет.
**Порядок между частями законен на стройке и подозрителен на доработке**, и это
единственное, что стадия здесь меняет. Беклог стройки **весь** состоит из
упорядоченных зависимостью шагов: «сперва А, потом Б» — не повод не дробить, а
описание того, как этот список устроен, и части просто встают подряд. На
доработке правки независимы, и обнаруженный порядок «иначе не собрать» чаще
всего значит, что перед тобой не декомпозиция, а план реализации: шаги остаются
**внутри одного файла**.
Не проходит хотя бы одно — **не дроби**. Ложная декомпозиция плодит файлы,
которые нельзя взять поодиночке, и переоценка потом склеивает их обратно.
## Где резать, если резать можно
Тест выше говорит, **допустим** ли разрез. Где его провести из нескольких
допустимых мест — отвечает шов.
**Шов — там, где меняется род работы.** Раздел «Затрагивает» перечисляет
границы; если одна строка перечня стоит особняком от остальных — трогает другой
слой, переносит ответственность, вводит новое понятие, — эта часть и режется
отдельно. Пример: задача перекладывает несколько узлов разом и заодно добавляет
два поля в существующий ответ; переложенная часть и добавленные поля проверяются
по-разному человеком, хотя конвейером — одинаково.
**Ревью на цену разреза больше не влияет.** Состав прогона постоянный: гейт,
спеки, код, триаж плюс приёмник тем, — и каждая половина платит его целиком.
Значит, разрез удваивает костяк ревью **всегда**, а не только когда обе половины
остаются в одной метке; выигрыш он даёт не в проверке, а в том, что каждая
половина доводится и мерджится сама по себе. Прежде здесь стояло правило «резать,
когда разрез снимает дорогой проход с большей части диффа» — снимать больше
нечего.
**Это планирование, а не предписание процесса.** Как проверять изменение, решает
конвейер, увидев его; в тело задачи это не пишется — строка «делать вот так» и
есть тот второй дом правила, который гигиена полей снимает.
## Что делать с родителем
После разделения родитель **не остаётся** третьей висящей строкой:
части полностью замещают его → `close <slug> --reason "разложена на a, b"`.
`REJECTED.md` здесь — не «выкинули», а именно тот след, что переживает запись:
через квартал вопрос «куда делась задача X» отвечается строкой со ссылками на
наследников, а не археологией git.
**Зонтика над частями нет никакого.** Тип `epic` упразднён, цель, игравшая его
роль после него, — тоже. Если частям нужен общий заголовок, у них общее место в
списке: они встают подряд, и соседство и есть тот ответ, ради которого заводили
зонтик.
## Когда декомпозиция случается посреди работы
Задача, которая **оказалась крупнее задачи**, распознаётся до того, как под неё
заведено предложение об изменении: иначе его придётся выбрасывать. Она выходит
из работы на декомпозицию, а её строка возвращается в беклог с причиной
(`move … --reason "крупнее задачи"`). **Место в списке частям назначает
человек**: машина поставит их в конец секции, а на стройке место наследуется от
родителя (`move --after`), да и на доработке крупная задача редко распадается на
что-то менее срочное, чем была сама.
## Мозговой штурм сырья
Сырьё — запись типа `research`, у которой раздел «Вопрос» пуст: она не проходит
тест «готова к взятию», потому что неясно, что именно делаем. Штурм проясняет —
и это **generative-операция, а не applicative**.
Исход штурма и есть заполненный «Вопрос» (тогда разведку можно брать в работу)
или набор задач с типами, которые из ответа следуют. Третий законный исход —
`close --reason`.
Applicative-штурм («перечисли задачи, следующие из идеи») выдаёт очевидное:
перечисляется то, что уже видно в формулировке. Ценное — на уровень выше.
1. **Сперва — формы, а не задачи.** Предложи **три разные постановки** идеи и
назови **компромисс каждой**: что она даёт, чем платит, что оставляет за
бортом. Если получилась одна постановка — штурм не состоялся, это
applicative.
2. **Вынеси формы пользователю** через `AskUserQuestion` с компромиссами. Рамку
выбирает он: это продуктовое решение, не механика.
3. **Назови пользу.** Выбранная форма отвечает на «что станет наблюдаемо иначе».
Идея, для которой такого ответа не находится, скорее всего уезжает в
`REJECTED.md`, а не заводится задачей.
4. **Только выбранную форму** дроби по тесту декомпозиции выше и проставь
критерии приёмки: без них наследники останутся идеями под другим именем.
**«Выкинуть» — полноправный исход штурма, а не его неудача.** Проработка, честно
показавшая, что пользы нет или она несоразмерна цене, — это результат: идея
уезжает с этой самой причиной, и та причина гасит её повторное появление.
## Доклад
- Идея/задача на входе, выбранная рамка (для штурма), задачи-наследники со
слагами, секциями и местом в списке.
- Судьба родителя: удалён / выкинут с причиной.
- `tasks.py check` после правок.
- Границы покрытия: какие постановки рассмотрены и какие сознательно отброшены —
чтобы штурм не пришлось повторять с нуля.
@@ -0,0 +1,73 @@
# 🧹 `chore` — обслуживание, наблюдаемое поведение не меняется
Зависимости, сборка, перенос, чистка, оснастка. Отвечает на **«что нужно
сделать»**, глаголом в неопределённой форме.
Общая форма записи (мета, слаг, строка индекса) — [task-format.md](task-format.md).
Здесь только то, что у этого типа своё.
## Схема
| | |
| --- | --- |
| Заголовок отвечает на | что нужно сделать |
| Обязательные разделы | `Затрагивает`, `Критерии приёмки` |
| Допустимые сверх того | `Рамки`, `Вопросы` |
| Поле места | **Категория** — полка домена беклога |
| Индекс | `BACKLOG.md` |
| Берётся в работу | да |
## Адресат — разработчик, и это законно
Тест готовности спрашивает «что станет наблюдаемо иначе». У `chore` ответ
адресован **разработчику**, а не пользователю: «перестанет собираться два раза»,
«уедет последний вызов устаревшего API», «проверки гоняются одной командой».
Это ответ, а не отговорка.
**У `chore` тест готовности слабее честно, а не молча.** Пока типа не было,
такие задачи либо не заводились вовсе, либо формулировались как выдуманная
пользовательская польза — и то и другое хуже, чем сказать прямо, для кого работа.
Отсюда же граница: если после задачи меняется то, что видит пользователь, — это
не `chore`. Тип, оставшийся от первой формулировки, врёт ровно там, где по нему
отбирают.
**Обнаружилось это уже в работе — запись переформулируется, а не дорешивается.**
Исполнитель останавливается, называет тип, которым задача оказалась (`fix`
поведение расходится с заявленным, `feature` — снаружи появляется то, чего не
было), и человек решает: сменить тип и решать процессом того типа — либо
прекратить. Тип меняет этот скилл, а не исполнитель по ходу: у нового типа своя
схема разделов, и `ready` проверит её заново.
## Алгоритм
1. **Проверить, что поведение не меняется.** Меняется — это `feature` или `fix`,
и у последнего другие требования (воспроизведение).
2. **Назвать, что перестанет мешать** — одной фразой, адресуясь разработчику.
«Прибраться в модуле X» — не ответ: непонятно, что изменится.
3. **Назвать границы** в `Затрагивает`. У обслуживания они часто не в коде:
конфиг и его образцы, версия зависимости, команда сборки, файл CI. Границей
считается то, у чего есть внешняя сторона и цена изменения.
4. **Написать критерии приёмки** — 2–5 утверждений с оракулами. У `chore`
оракул обычно самый дешёвый из всех типов: команда, которая раньше падала
или требовала трёх шагов, теперь отрабатывает одним.
5. **Проверить, что это не «заодно».** Обслуживание любит склеиваться в пачку
(«обновить зависимости и переписать сборку и убрать мёртвый код»). Не
мерджится порознь — это несколько задач ([split.md](split.md)).
## Кто такую задачу решает
Решает её конвейер проекта — скилл `av-dev:code-resolve`,
**сценарий обслуживания**: он не заводит change и не пишет требований, потому что
у работы, не меняющей поведения, дельта-спек нет по построению. Тип записи там
только **предлагает** сценарий, а подтверждает его отсутствие дельт: разошлись —
работа останавливается, и это тот самый случай, когда тип, оставшийся от первой
формулировки, врёт. Задачу ведут не этим процессом — она решается как проект
привык, а этот скилл её только заводит и закрывает.
## Что видит машина, а что человек
`ready` смотрит на **наличие непустого** `Затрагивает` и на
**число** критериев — ровно то же, что у `feature`. Разница между типами здесь не
в строгости проверки, а в том, **кому адресован ответ** на «что станет
наблюдаемо иначе», — и это судит человек.
@@ -0,0 +1,59 @@
# ✨ `feature` — снаружи появляется то, чего не было
Задача, после которой наблюдаемое поведение меняется в сторону новой
возможности. Отвечает на **«что нужно сделать»** и пишется глаголом в
неопределённой форме.
Общая форма записи (мета, слаг, строка индекса) — [task-format.md](task-format.md).
Здесь только то, что у этого типа своё.
## Схема
| | |
| --- | --- |
| Заголовок отвечает на | что нужно сделать («Печатать поле одним куском кода») |
| Обязательные разделы | `Затрагивает`, `Критерии приёмки` |
| Допустимые сверх того | `Рамки`, `Вопросы` |
| Поле места | **Категория** — полка домена беклога |
| Индекс | `BACKLOG.md` |
| Берётся в работу | да |
## Алгоритм
1. **Проверить, что возможность и правда новая.** Поведение расходится с уже
заявленным — это `fix`, а не `feature`, и требования у него другие.
2. **Назвать границы** в разделе `Затрагивает`: эндпоинт или команда, таблица и
миграция, формат на диске, публичный тип пакета, внешний сервис. Названы
**границы, а не замысел**: «переписать хранилище на новый драйвер» — замысел,
`таблица points и её миграция` — граница. Проверяется вопросом «это можно
назвать до того, как решено *как* делать?».
3. **Написать критерии приёмки** — 2–5 проверяемых утверждений списком, у
каждого назван оракул. Не «работает корректно», а «повторный прогон даёт тот
же отпечаток — оракул: команда сверки».
4. **Поставить её на место в списке.** На стройке место называет зависимость:
`move <слаг> --after <шаг, без которого нельзя>`. На доработке место в
очереди назначает груминг, и конец списка законен.
5. **Проверить, что задача одна.** Отвечается всё, но задача не делается одним
заходом и не мерджится целиком — это несколько задач, дроби сразу
([split.md](split.md)) и ставь их в списке подряд.
6. **Реализация** — дело конвейера проекта, не этого скилла. Закрывается
`close <слаг> --implemented`: файл и строка удаляются, суть переезжает в
`openspec/specs/` и документацию.
## Что видит машина, а что человек
Схему типа судит `ready` на входе в работу: **наличие непустого** раздела
`Затрагивает` и **число** критериев (меньше двух — отказ, больше пяти —
замечание). Наличие оракула проверяется **эвристикой** — словом «оракул»
в пункте. `check` этого поимённо не говорит, а считает строкой здоровья
(`SKILL.md`, «Что механизировано, а что нет»).
Полнота перечня границ машине не видна: границу, которую забыли назвать, она от
отсутствующей не отличает. Настоящий оракул от слова «оракул» тоже не отличает.
Поэтому в докладе это называется как есть: «проверено число пунктов и наличие
границ, годность оракулов и полнота границ — глазами».
**Критерии — пол, но расхождение с ними есть дефект критериев.** Видишь, что
критерии закрыты, а суть задачи не достигнута — **правь критерии и возвращай
задачу**, а не держи невидимое сверх-требование: иначе исполнитель никогда не
знает, закончил ли, и мотивирован занижать критерии заранее.
@@ -0,0 +1,66 @@
# 🐞 `fix` — поведение расходится с заявленным
Задача о расхождении между тем, что система делает, и тем, что про неё заявлено
— в спеке, в инварианте `CLAUDE.md`, в критериях закрытой задачи. Отвечает на
**«что нужно сделать»**, глаголом в неопределённой форме, перед ним допускается
«не»: «Не отбрасывать молча лишние символы в ходе».
Общая форма записи (мета, слаг, строка индекса) — [task-format.md](task-format.md).
Здесь только то, что у этого типа своё.
## Схема
| | |
| --- | --- |
| Заголовок отвечает на | что нужно сделать |
| Обязательные разделы | **`Воспроизведение`**, `Затрагивает`, `Критерии приёмки` |
| Допустимые сверх того | `Рамки`, `Вопросы` |
| Поле места | **Категория** — полка домена беклога |
| Индекс | `BACKLOG.md` |
| Берётся в работу | да |
## `Воспроизведение` — раздел, которого нет у других типов
**Не воспроизводится — это `research`, а не `fix`.** Правило было записано и
раньше, но проверять его было нечем, и «починки» без единого шага повторения
уходили в работу наравне с остальными. Раздел делает правило проверяемым: он
называет, **что сделать, чтобы расхождение проявилось, и что при этом видно
вместо ожидаемого**.
Пишется двумя частями, обе обязательны по смыслу:
- **шаги или вход** — команда, запрос, файл, последовательность действий;
- **что видно и что ожидалось** — «ввод `а1б2` ходит в `a1`, а должен быть
отвергнут с ошибкой».
Это не критерии приёмки и не дублирует их: воспроизведение описывает **сегодня**,
критерии — **завтра**. Пропущенное воспроизведение чаще всего означает одно из
двух: расхождение приняли на слово, или его вообще нет, а есть недовольство
поведением — и тогда это `feature`, а не `fix`.
## Алгоритм
1. **Воспроизвести.** Не удаётся — это `research`: заведи вопрос «при каких
условиях проявляется» и не притворяйся, что чинить есть что.
2. **Найти, чему поведение противоречит.** Спека, инвариант, критерий закрытой
задачи. Не противоречит ничему — это `feature`: поведение никогда и не было
заявлено, а тип, оставшийся от первой формулировки, врёт ровно там, где по
нему отбирают.
3. **Записать воспроизведение** — шаги и наблюдаемое против ожидаемого.
4. **Назвать границы** в `Затрагивает`: починка часто трогает больше, чем
кажется по объёму текста, и оценка систематически занижена именно здесь.
5. **Написать критерии приёмки** — 2–5 утверждений с оракулами. У починки
почти всегда есть парный критерий: **прежнее поведение не сломалось**
(«ввод `а1` принимается по-прежнему»). Без него починка чинит одно и ломает
соседнее.
6. **Записать дефект в журнал** `docs/review.md` с пометкой «проскочил / пойман
ревью». Проскочившие — проверочный набор для калибровки конвейера; пойманные с
оракулом — лучшая опора для прохода ревью: проектные, воспроизводимые,
однажды оказавшиеся правдой.
## Что видит машина, а что человек
`ready` смотрит на **наличие непустого** `Воспроизведения` и
`Затрагивает` и на **число** критериев. Годность воспроизведения — человеку:
шаги, по которым ничего не воспроизводится, машина от годных не отличает, и
делать вид, что проверено больше проверенного, хуже, чем не проверять вовсе.
@@ -0,0 +1,343 @@
# Формат записей и индекса
Заголовок, мета-блок и строку индекса ставит `tasks.py add` — руками их не
пишут. Этот файл описывает **общую форму** любой записи и то, что проверяет
`check`; тело дописывает агент.
Чем разделы тела отличаются от типа к типу, какой алгоритм у каждого типа и что
у него обязательно — **отдельным файлом на тип**:
| Тип | Файл | Одной строкой |
| --- | --- | --- |
| ✨ `feature` | [task-feature.md](task-feature.md) | снаружи появляется то, чего не было |
| 🐞 `fix` | [task-fix.md](task-fix.md) | поведение расходится с заявленным |
| 🧹 `chore` | [task-chore.md](task-chore.md) | обслуживание, поведение не меняется |
| 🔬 `research` | [task-research.md](task-research.md) | исход — знание, а не изменение |
## Файл записи
`items/<slug>.md`:
```markdown
# 🐞 Не отбрасывать молча лишние символы в ходе
- **Тип:** fix
- **Категория:** Ядро — вернулась из работы: остаток писал нерешённое в журнал
- **Зачем:** ввод «а1б2» ходит в a1 — игрок не видит, что ошибся, и винит игру
Разбор хода читает первые два символа и молча выбрасывает остаток строки.
## Воспроизведение
Ввести `а1б2` в свой ход: программа ходит в `a1` и ничего не сообщает.
Ожидалось — отказ с ошибкой разбора.
## Затрагивает
Разбор строки хода; текст ошибки в выводе партии. Формат сохранения партии
не трогается.
## Критерии приёмки
- ввод «а1б2» отвергается с ошибкой — оракул: тест разбора
- ввод «а1» принимается по-прежнему — оракул: тест разбора
## Рамки
Схема не трогается; данные только читаются; перезапуск допустим.
Связано: решение о канонической форме содержимого.
```
- **Заголовок H1** — он же заголовок строки в индексе, дословно. Начинается
**эмодзи типа**, и она **производна**: её ставит `add` и чинит `check --fix`
по полю меты. Второго дома у типа нет — эмодзи это его отображение, как
строка индекса это отображение файла.
- **Форма заголовка — по типу.** `feature`, `fix` и `chore` отвечают на «что
нужно сделать», глаголом в неопределённой
форме, перед ним допускается «не»; `research` называет предмет разведки и
формы действия **не несёт намеренно**. Почему так — SKILL.md, «Как написана
задача». `check` считает заголовки не в форме действия и печатает число в
здоровье; годность формулировки смотрит агент `task-form`.
- **Мета-блок** — список сразу после заголовка, **поле на строку**. Обязательны
**тип** и **место**, причина после тире желательна (именно она объясняет,
почему задача здесь оказалась — в том числе «вернулась из работы: …»), «зачем» и
теги необязательны. Нераспознанные поля сохраняются: скрипт правит свои и не
трогает чужие.
- **Тип — первым полем.** Он решает, что у записи вообще может быть: какие
разделы обязательны и берётся ли она в работу, — и читается раньше всего
остального. Словарь **закрыт**: `feature` | `fix` | `chore` | `research`. Не подходит ни один — это сигнал, что в записи их два и
её надо разделить.
- **«Зачем» отвечает на «зачем нужна эта задача»** — состояние, остаток, боль.
Не пересказ задачи: пересказ уже есть по ссылке. Живёт здесь, а не только в
индексе: строка индекса его повторяет и производна от него, `check` сверяет,
`check --fix` восстанавливает пропавшую строку **вместе с ним**. Пока поле
лежало только в индексе, штатная починка дрейфа теряла его молча и
навсегда — а это единственное, по чему задачу выбирают, не открывая.
- **Тело** — одна фраза «что станет наблюдаемо иначе», дальше разделы по схеме
типа. Пишется на языке документации проекта: предметно, без англицизмов, у
которых есть русское слово, и без терминов, которых нет ни в паспорте, ни в
архитектуре, ни в конвенциях (правило и его причина — в SKILL.md, раздел «Как
написана задача»).
Тело — не план реализации и не спецификация: принятое и реализованное переезжает
в документацию проекта, а файл задачи удаляется.
### Поле места: «Категория»
Поле называет **секцию беклога, в которой числится строка** — полку домена
(`Ядро`, `Инфра`, …), куда задачу положили и куда вернут, если она уйдёт в работу
и вернётся. **На стройке секция одна**, и поле называет её же: различать ей
нечего, но производность от заголовка индекса сохраняется и там.
Прежнее имя поля — **«Секция»**: так оно называлось у целей, указывая на часть
роадмапа. Разбор его по-прежнему принимает, `check` называет дрейфом, `check
--fix` переименовывает.
Имя самого места принадлежит **заголовку индекса** — файл на него лишь
ссылается, и принадлежность сверяется по нижнему регистру.
### Прежние формы, которые читаются, но не пишутся
Всё это `check` называет дрейфом, а `check --fix` переписывает:
| Было | Стало |
| --- | --- |
| префикс `[goal]` / `[idea]` в H1 | поле **Тип** + эмодзи в H1; `[idea]``research` |
| тег `kind:<род>` | поле **Тип** (род работы стал типом) |
| поле **Секция** | поле **Категория** |
| теги `goal:<слаг>` и `decomposed` | сняты: целей больше нет |
| поле **Хук** | поле **Зачем** |
| мета одной строкой через `·` | мета списком, поле на строку |
Чего `--fix` не делает сам — **решает за человека, каким быть типу**. Случаев
три, и все три уезжают пометкой `НЕОДНОЗНАЧНО`: тип, которого неоткуда взять
(`feature` от `chore` машина не отличает); тип вне словаря; и запись типа `goal`
— целей больше нет, а во что превращается эта, в задачу или в ничто, машина не
знает. Подставленное наугад значение врало бы ровно там, где по нему принимают
решение. Мёртвые теги и имя поля места при этом снимаются у **любой** записи,
включая ту, чей тип остался неразобранным.
### Затрагивает
Перечень **границ**, которых изменение касается. Границей считается то, у чего
есть внешняя сторона и цена изменения:
- эндпоинт, команда, форма ответа, код ответа;
- таблица, поле, миграция, формат на диске, формат сообщения в очереди;
- публичный тип или функция пакета, конфиг и его образцы;
- внешний сервис или библиотека, чьё поведение становится нужным.
Ничего из этого не трогается — так и пишется: «границ не трогает, изменение
внутри одного узла». Это ответ, а не пустой раздел.
**Границы, а не замысел.** «Переписать хранилище на новый драйвер» — замысел;
`таблица points и её миграция`, `эндпоинт POST /ingest` — границы. Разница
проверяется вопросом «это можно назвать до того, как решено *как* делать?»: если
нет, строка описывает реализацию, и её место в предложении об изменении.
**Свойства репозитория сюда не пишутся** — по той же причине, что и в рамки:
имя таблицы стабильно, номер последней миграции протухает молча. Пишется
`таблица points и её миграция`, а не `миграция 0042`.
**Что из этого механизировано.** `ready` смотрит только на
**наличие непустого раздела**. Полнота перечня машине не видна: границу, которую
забыли назвать, она от отсутствующей не отличает. Раздела нет — отказ во взятии:
оценивать нечем.
**У `research` раздела нет** — её границы становятся известны, когда из разведки
родятся задачи.
### Критерии приёмки
2–5 проверяемых утверждений **списком** `- …`, **у каждого назван оракул**. Не
«работает корректно», а «повторный прогон даёт тот же отпечаток — оракул:
команда сверки». Это не второе определение сделанного, а проектная
конкретизация вопроса «по чему видно, что закончено» из теста готовности ниже:
там сказано «признак завершённости», здесь — «признак плюс чем проверяется».
**Что из этого механизировано.** `ready` считает пункты: меньше
двух — отказ («— работает» одной строкой больше не проходит), больше пяти —
замечание, обычно это признак, что задача крупнее задачи. Наличие оракула
проверяется **эвристикой** — словом «оракул» в пункте, — и потому даёт только
замечание: настоящий оракул от слова «оракул» машина не отличает, и делать вид,
что проверено больше проверенного, хуже, чем не проверять вовсе.
**У `research` критериев нет** — её приёмка это записанный ответ, и описывается
она разделами «Вопрос» и «Куда ляжет ответ».
**Критерии — пол, но расхождение с ними есть дефект критериев.** Если приёмщик
видит, что критерии закрыты, а суть задачи не достигнута, он **правит критерии и
возвращает задачу исполнителю**, а не держит невидимое сверх-требование. Иначе
исполнитель никогда не знает, закончил ли, и мотивирован занижать критерии
заранее.
### Рамки
Одна строка: чего касаться нельзя, что перезапускается, что считается
необратимым, трогается ли схема данных. Раздел **допустим у любого типа задачи и
ни у одного не обязателен**. **Свойства репозитория сюда не пишутся** — номер
последней миграции, версия зависимости, хеш: в лежалой задаче они протухают
молча и становятся ложной рамкой. Снимок берётся при постановке, а не при
заведении.
### Вопросы
Неразобранное решение человека живёт разделом `## Вопросы` **плюс тегом
`question`**. Раздел без тега или тег без раздела — дрейф, `check` о нём скажет.
Раздел `Вопросы` (о решении человека) и раздел `Вопрос` у `research` (предмет
разведки) — **разные вещи и разные слова**: первый блокирует взятие, второй его
разрешает.
**Судит факт, а не метка.** Отказ во взятии даёт **непустой раздел «Вопросы»**,
независимо от того, стоит ли тег: иначе забывший тег проходил бы, а поставивший
спотыкался — стимул ровно обратный записанному правилу. Тег производен: он нужен
отбору снаружи файла (`list --questions`, `list --tag question`), и его
отсутствие при непустом разделе — замечание, а не лазейка. Тег без раздела тоже
отказ, но с другим советом: либо вопрос записан не туда, либо тег пора снять.
**Ответ на вопрос — три правки, и первая обязательна.** Раздел «Вопросы»
опустошается: ответ переезжает в тело решением, а не остаётся вопросом рядом с
ответом. Затем снимается тег (`edit <slug> --rm-tag question`) и переписывается
«зачем»: «Решено: …» на вопрос «зачем нужна эта задача» уже не отвечает.
**Порядок именно такой, потому что судит раздел, а не тег.** `ready`
смотрит в непустой раздел и откажет даже при снятом теге, а `check`
на снятый тег при непустом разделе посоветует тег вернуть. Снять тег, не
опустошив раздел, — значит закольцевать себя между двумя советами.
## Слаг
Латиница и цифры, kebab-case, без ведущих, хвостовых и двойных дефисов
(`foo-bar`, не `-foo`, `a--b`). Именуется **по сути, а не по текущей
формулировке**: заголовок будет переписан при переоценке, а слаг стоит в ссылках
из других задач, коммитов и черновиков. **Транслита не заводим**
`tie-break-equal-completeness`, а не `taj-brejk-pri-ravnoj-polnote`: транслит
нечитаем для того, кто ищет по смыслу, и не сокращается.
Переименование слага — не правка, а перенос ссылок: делается одним атомарным
проходом по всем местам, где слаг упомянут, иначе останутся битые ссылки,
которых никто не проверяет.
## Индекс
Строка одной формы:
```markdown
- [🐞 Заголовок дословно](items/slug.md) — зачем
```
«Зачем» отвечает на «зачем нужна эта задача» одним предложением: состояние,
остаток, боль. Пересказ первого абзаца бесполезен — он уже есть по ссылке.
Эмодзи внутри квадратных скобок не украшение: заголовок копируется **дословно**,
и тип виден там, где решают «брать или не брать».
| Файл | Что отвечает | Секции |
| --- | --- | --- |
| `BACKLOG.md` | что **можно взять**, в значимом порядке | называет проект; на стройке ровно одна (умолчание `План`), на доработке сколько нужно (умолчание `Ядро`/`Инфра`) |
| `REJECTED.md` | что ушло без реализации и почему | — |
Секции — **единственные заголовки `##` в индексе**: любой другой `##` в
преамбуле проверка сочтёт секцией.
**Порядок строк внутри секции значим, и стадия решает, что он значит:** на
стройке зависимость, на доработке важность (SKILL.md, «Две стадии»). Назначает
его человек — раскладывая шаги или на груминге, — и двигают его `move --after`
и `move --first`. Одно место из очереди изъято и **производно от типа и
заполненности**: **сырьё** (`research` без раздела «Вопрос») стоит в конце своей
секции, потому что его не берут, и между берущимся оно каждый раз требует
открыть файл, чтобы это понять. Проверяет `check`, переставляет `check --fix`,
и человек этот порядок не назначает — иначе он был бы решением, которого здесь
нет.
**Секции «блокеры» среди них нет.** Блокер — состояние, а не полка: он живёт до
ответа человека, а следы остаются вопросами в файлах задач.
Постоянно пустая секция со старой семантикой «разбираются пачками» противоречила
бы правилу «эскалируем немедленно», поэтому `init` её не заводит, а `check`
говорит о ней в чужом беклоге. Переезжаешь с такой секцией — удали её.
**Имена секций проект выбирает сам, а количество ограничено стадией:** на
стройке секция одна, потому что порядок там зависимость, и разложенный по полкам
список перестаёт быть планом. Проверяет `check`; слить секции сам он не берётся —
в каком порядке пойдут строки слитых полок, знает только человек.
**Заголовок секции пишется с прописной и отбивается пустой строкой с обеих
сторон.** Отбивку правит `check --fix`; он же сводит написание места в мете файла
с заголовком индекса.
Индекс **производен**: расходится с файлом — правим индекс (`check --fix`).
Строку руками не пишут.
Отсюда же ответ на «а если оборвётся посередине». Мутация сперва проверяет всё
и складывает правки, и только потом пишет: сначала все временные файлы, потом
переименования подряд. Полной транзакции на несколько файлов файловая система не
даёт, но окно сжато до цепочки переименований, а **всё, что в нём может
разъехаться, — производное**: файлы целы, индекс восстанавливает `check --fix`.
Поэтому отказ на второй задаче из пяти не оставляет первую переписанной при
нетронутом индексе.
## `REJECTED.md`
Туда уходит задача, покинувшая беклог **без реализации**. Строку пишет
`tasks.py close --reason`, а `check` следит за форматом:
```markdown
- 2026-07-23 `versii-kachestvo-repaki` — Версии и качество одного тайтла.
Причина: калибровка болей — не боль, ни разу не возникло за полгода.
Была секция: Инфра.
```
Реализованные сюда не попадают: у них остаётся коммит и документация. У
выкинутой не остаётся ничего — и через квартал она возвращается тем же текстом.
Это первое место, куда смотрит дедупликация при заведении.
Запись не запрещает завести задачу заново: изменился контекст — заводим и
ссылаемся на строку, объясняя, что изменилось.
## Теги
Разметка сверх типа. Тип полем, потому что он один и обязателен; теги — потому
что их много и `list --tag` уже умеет отбирать по ним порцию разбора.
- `question` — в файле есть неразобранный раздел «Вопросы».
Тегов `kind:<род>`, `goal:<слаг>` и `decomposed` больше нет: род работы стал
типом, а цели упразднены. Оставшиеся в файле `check` называет дрейфом, а `check
--fix` снимает (значение `kind:` при этом переезжает в поле «Тип»).
Отбор — `list --tag a,b`: перечисленные через запятую теги требуются **все
сразу** (это И, не ИЛИ). Тег, которого нет ни у одной задачи, `list` называет
вслух: молчаливый ноль читается как «таких задач нет», а чаще это опечатка.
Свои теги проект заводит свободно (партия ревью `review-ГГГГ-ММ-ДД`, тема,
источник) — словарь не фиксирован. В индекс теги не выносим: он
производен, отбор делает `list --tag`, а не глаза.
## Тест «готова к взятию»
Задача готова, если из файла отвечаются три вопроса. Первый общий, второй и
третий у каждого типа свои и перечислены в его файле.
1. **Что станет наблюдаемо иначе**, когда она сделана — снаружи: пользователю,
владельцу сервиса или разработчику. «Отрефакторить X» — не ответ; «перестанет
ломаться Y при Z» — ответ. **У `chore` адресат — разработчик, и это
законно**: «уедет последний вызов устаревшего API» — ответ, а не отговорка.
Тип объявлен как раз затем, чтобы такие задачи не выдумывали себе
пользовательскую пользу.
2. **Что известно про сегодня** — то, что тип требует знать до работы:
у `fix` это `Воспроизведение`, у `research``Вопрос`, у `feature` и
`chore``Затрагивает`.
3. **По чему видно, что закончено** — критерии приёмки с оракулами;
у `research` вместо них `Куда ляжет ответ`.
Не отвечается любой из трёх → это ещё не задача, а **сырьё**: тип `research` без
раздела «Вопрос», место — конец секции, работа над ним — штурм.
Отвечается всё, но задача не делается одним заходом и не мерджится целиком →
это **несколько задач**, дроби сразу и ставь их в списке подряд. Промежуточного
зонтика между планом и задачей нет: тип `[epic]` упразднён, и цель, ставшая
зонтиком после него, упразднена тоже.
Тест применяется при заведении и при переоценке. К старым задачам, которых
операция не касается, задним числом не применяется — беклог не переоформляют
«заодно».
@@ -0,0 +1,86 @@
# 🔬 `research` — исход работы знание, а не изменение системы
Ответ на вопрос, замер, разведка, проработка сырой мысли. Приёмка — **записанный
ответ**, а не изменённый код.
Общая форма записи (мета, слаг, строка индекса) — [task-format.md](task-format.md).
Здесь только то, что у этого типа своё.
## Схема
| | |
| --- | --- |
| Заголовок отвечает на | о чём разведка (предмет, а не действие) |
| Обязательные разделы | `Вопрос`, `Куда ляжет ответ` |
| Допустимые сверх того | `Рамки`, `Вопросы` |
| Поле места | **Категория** — полка домена беклога |
| Индекс | `BACKLOG.md` |
| Берётся в работу | да — **но только с заполненным «Вопросом»** |
**Критериев приёмки у `research` нет, и это не поблажка.** Критерии в форме
«оракул: тест» разведке натянуты: проверять нечего, пока ответа нет. Её приёмка
описывается раздельно — вопрос, на который отвечаем, и место, куда ляжет ответ.
**Заголовок формы действия не несёт намеренно.** Что делать, ещё неизвестно, и
заголовок-действие обещал бы решённость, которой нет. «Подсказка следующего
хода», а не «Сделать подсказку следующего хода».
## Этот тип вобрал прежний `[idea]`
Тип `idea` упразднён. Он значил не род работы, а **состояние незаполненности**
«первый, второй или третий вопрос теста готовности не отвечается», — а состояние
типом быть не может: оно меняется по мере того, как запись дописывают, а тип
меняют командой.
Теперь это состояние называется честно: **`research` без раздела «Вопрос» — это
сырьё**.
| | сырьё | разведка |
| --- | --- | --- |
| Раздел `Вопрос` | пуст или отсутствует | заполнен |
| `ready` | отказ | берёт |
| Место в секции беклога | **конец**, `check --fix` сносит туда сам | среди прочих |
| `tasks.py list --raw` | показывает | нет |
Порядок строк в беклоге назначает человек, и стадия решает, что он значит:
зависимость на стройке, важность на доработке (правило 4 скилла).
Место сырья **из него изъято**: оно производно от типа и заполненности, а не от
чьего-то решения, и потому его проверяет и чинит машина. Приоритетом оно не
становится: сырьё не берут вовсе, и место в конце говорит именно это.
Сырьём заводится и **сырая функция**: «Подсказка следующего хода» — ещё не
`feature`, потому что неизвестно, что именно делать. Работа над ней — думание, и
её исход — либо задачи, либо отказ.
## Алгоритм
1. **Записать вопрос одной фразой.** Не тему, а вопрос: не «Разобраться с
выводом в терминалах», а «Какими символами рамки печатаются одинаково в
Терминале, iTerm и `tmux`». Вопроса ещё нет — запись заводится сырьём и
лежит в конце секции, пока вопрос не появится.
2. **Назвать, куда ляжет ответ**: `docs/research/<slug>.md`, ADR, тело этой
задачи. Место называется **заранее**, иначе ответ остаётся в переписке, а
через квартал разведку заказывают заново.
3. **Ограничить рамками**, если разведка может утечь: сколько времени, какие
источники, что заведомо вне.
4. **Провести разведку** и **записать ответ по названному адресу**. Числа — с
происхождением: с командой или условиями, которыми получены. Число без источника
проход ревью обязан читать как условие, а не как замер. Проводит её конвейер
проекта — скилл `av-dev:code-resolve`, сценарий разведки; этот скилл её
только заводит и закрывает.
5. **Разложить исход на задачи** — если он их родил. Разведка кончается одним из
трёх: заведены задачи, записано знание, отказ. **Отказ — полноправный исход**:
«проверили, не проблема» экономит работу.
6. **Закрыть**`close <слаг> --implemented`, когда ответ записан. Файл
удаляется: запись ответа и есть след, второго не нужно. Ушла без ответа —
`close --reason`, и строка уезжает в `REJECTED.md`.
## Что видит машина, а что человек
`ready` смотрит на **наличие непустых** разделов `Вопрос` и
`Куда ляжет ответ`; `check` считает сырьё отдельной строкой здоровья и держит
его в конце секции. Годность вопроса — человеку: «вопрос это или тема» машина не различает,
и `check` о годности молчит намеренно.
Штурм сырья, дробление исхода на задачи и тест «части мерджатся порознь» —
[split.md](split.md).
File diff suppressed because it is too large Load Diff
+86
View File
@@ -0,0 +1,86 @@
# 1. Статус OpenSpec (2026-08-03)
## Что было
OpenSpec несёт оба проекта: healthlog — 5 capability, 3530 строк спек, 9
архивных change за две недели; jellybit — 11 capability, 3895 строк, 43 архивных
change. При этом в трёх местах плагина написана ветка «проект без OpenSpec»
(`task-pipeline` предпосылки, `review-pipeline` предпосылки, `task-batch`
предпосылки) — и **не исполнялась ни разу**.
Проектные факты живут в пяти домах: `CLAUDE.md`, `docs/architecture.md`,
`openspec/specs/`, `openspec/config.yaml``context`, и планируется шестой —
`docs/review-brief.md`.
Расхождение измерено: у healthlog раздел «Хранилище» в `docs/architecture.md`
950 строк (377–1328) против `openspec/specs/storage/spec.md` на 1337 строк. Два
описания одного поведения, никем не сверяемые. У jellybit того же нет:
`docs/specs/architecture.md` — 300 строк обзора, детали в 11 спеках. **Проект с
43 изменениями держит архитектуру втрое короче проекта с 9.**
## Решено
**Р1. OpenSpec — жёсткая предпосылка `av-dev-pipeline`.** Ветки деградации
удаляются, вместо них объявленная зависимость и проверка на старте. Зависимость
на уровне **плагина, а не процесса**: `av-dev-tasks`, `av-dev-git` и будущий
плагин документов от OpenSpec не зависят и работают на python/ansible-проектах.
*Причина:* непроверенная ветка деградации хуже честной строки «требуется
OpenSpec» — она даёт ложную уверенность, что проект без спек поедет.
**Р2. Нормативный дом поведения — `openspec/specs/`.** `architecture.md`
переопределяется как **обзор**: принципы, компоненты со ссылками на capability,
внешние форматы данных, раскладка, деплой, открытые вопросы. Поведения он не
описывает.
*Причина:* `opsx:archive` вливает дельты именно в `openspec/specs/` — любой
другой нормативный дом обязан синхронизироваться руками и разойдётся. Форма
jellybit это уже подтвердила на 43 изменениях.
**Р3. `openspec/config.yaml``context` держит только нужды генерации.** Язык,
правила именования capability, придирки валидатора RFC 2119 — и ссылки. Правило
ревью, пересказ конвенций и инварианты оттуда вычищаются: у них есть свои дома.
*Причина:* блок «Ревью (процесс, не артефакт)» в обоих `config.yaml` дословно
повторяет шаги 4 и 7 `task-pipeline`. Это второй дом для правила, которым владеет
плагин, и он разойдётся на первой же правке.
## Что из этого следует
Из Р1:
**С1.** Три места с веткой деградации переписываются на объявленную предпосылку
плюс проверку на старте (есть `openspec/`, разрешаются `opsx:*`) и внятный
отказ: `task-pipeline` предпосылки, `review-pipeline` предпосылки, `task-batch`
предпосылки.
**С2.** Описание `av-dev-pipeline` в маркетплейсе получает строку «требует
OpenSpec».
**С3. Факт для темы «объединять ли tasks и pipeline»:** объединение потянуло бы
зависимость от OpenSpec на управление задачами, которой там сейчас нет.
Из Р2:
**С4.** Правило «поведение — в спеку, устройство и границы — в архитектуру»
становится контрактом плагина документов и правилом шага «синк документации» в
`task-pipeline`.
**С5.** healthlog чистится **не разом**: раздел вычищается той задачей, которая
его касается. Нужен способ не потерять остаток — иначе 950 строк «Хранилища»
останутся навсегда.
**С6. Дыра, которую решение открывает:** «почему» после архивации. Сегодня
`CLAUDE.md` healthlog велит писать причину решения в `architecture.md`а мы её
оттуда выселяем. Спеки нормативны и «почему» не держат; `design.md` живёт внутри
change и уезжает в архив. Либо ADR (как у jellybit), либо явное правило «почему
живёт в архивных change». **Первый вопрос следующей темы.**
Из Р3:
**С7.** `av-dev-pipeline` даёт образец `openspec/config.yaml` отдельным
reference — он владеет связью с OpenSpec. Заполняется при старте проекта и при
`adopt`.
**С8.** У обоих проектов из `config.yaml` вычищается блок «Ревью (процесс, не
артефакт)», пересказ конвенций и инвариантов.
+106
View File
@@ -0,0 +1,106 @@
# 2. Канон документов проекта (2026-08-03)
## Что было
Измерено по обоим проектам:
- **«Почему» не теряется — оно не находится.** `design.md` пишется почти всегда
(jellybit 39 из 43 архивных change, healthlog 9 из 9 — ≈285 КБ за две недели)
и имеет секции `Context` / `Goals / Non-Goals` / `Decisions` /
`Risks / Trade-offs`, то есть является ADR по структуре. Против этого ADR
руками: **6 записей у jellybit, четыре из них 13 июня — в день старта**; между
15 июня и 23 июля прошло ~40 изменений и ноль ADR. У healthlog ADR нет вовсе,
а настоящее ADR-рассуждение (отказ от DuckDB) лежит в разделе «Открытые
вопросы» файла `architecture.md`, потому что больше некуда.
- **Два плана.** `docs/plan.md` healthlog («порядок и его обоснование», 11 шагов)
и `<tasks>/PLAN.md` из `av-dev-tasks` («цели с обоснованием очереди прозой»)
— один артефакт под двумя именами.
- **Дубли спек у jellybit.** Из шести файлов `docs/specs/` три (`recognition`,
`review-ux`, `workflow`) описывают поведение, уже покрытое capability в
`openspec/specs/`.
- **`docs/drafts/` раскладывается без остатка:** `roadmap.md` → цели в «порядок»,
`conventions-backlog.md` → задачи `[idea]`, `logical-title-model.md` (293
строки, итог «сущность `title` не вводим») → намеренный отказ, то есть ADR.
## Решено
**Р4. «Почему» — ADR как промоут поверх архива.** Обоснование по-прежнему пишет
`design.md`; ADR — короткая запись, цитирующая решение и ссылающаяся на архивный
`design.md`. Заводит её **шаг «синк документации» пайплайна по названному
триггеру** (дорогой откат / намеренный отказ от очевидного / пересмотр прежнего
решения), а не человек по вдохновению.
*Причина:* ручной ритуал эмпирически не выжил — 6 записей на 52 изменения.
Автоматический (`opsx:propose` пишет `design.md` всегда) работает и производит на
порядок больше. Чинить надо не дом, а индекс и критерий промоута.
**Р5. `docs/plan.md` растворяется в `<tasks>/PLAN.md`.** Файл удаляется, 11
шагов становятся целями в «порядке», ссылки в `CLAUDE.md` и паспорте
переводятся.
**Р6. Пути жёсткие, оба проекта приводятся к одному виду.** Плагин знает
раскладку поимённо; указателя вида `.docs.json` нет.
*Причина (словами владельца):* «так проще ориентироваться во множестве проектов,
а не видеть слегка похожую, но разную структуру в каждом. Все проекты малого и
среднего размера, проще подогнать их под одну структуру. Кроме того, у OpenSpec
тоже структура строгая». Цена принята сознательно: плагин перестаёт быть
переносимым на чужой репозиторий, а `adopt` из «поправь указатели» превращается в
«перенеси файлы».
**Р7. Конвенции и разведка — каталогами с README-индексом.** `docs/conventions/`
и `docs/research/`: путь жёсткий, нарезка внутри свободна. Схема хранилища —
**отдельный** `docs/database.md` (своя каденция: меняется миграцией, а не
архитектурным решением; гейт healthlog уже сверяет миграции с документацией).
Конвенции идентификаторов и именования — не схема, они в `conventions/`.
**Р8. Слота для черновиков нет.** Идея → задача `[idea]`; намеренный отказ →
ADR; порядок работ → `PLAN.md`; незрелое размышление → `opsx:explore` внутри
change.
## Канон
```
CLAUDE.md памятка агенту: что это, стек, инварианты, команды, слоты
docs/
passport.md зачем и для кого; чем НЕ является; сценарии; референсы
architecture.md как сложено — обзор: принципы, компоненты со ссылками
на capability, внешние границы, раскладка, деплой
database.md схема хранилища (там, где есть БД)
conventions/README.md + <тема>.md как пишем код; README держит правило промоута
research/README.md + <тема>.md что показала реальность: чужие форматы, живые данные
adr/README.md + template.md + ADR-*.md почему — промоут поверх архивных design.md
review-journal.md промахи конвейера ревью ← уточнено в теме 3
review-brief.md предмет ревью — см. тему 3 ← отменено в теме 3
tasks/ av-dev-tasks: items/, PLAN.md, BACKLOG.md, SPRINT.md, REJECTED.md
openspec/
config.yaml только нужды генерации + ссылки
specs/<capability>/spec.md что система делает — нормативно
changes/archive/ журнал изменений с design.md — сырьё для ADR
```
Слотов **нет** у: `docs/drafts/`, `docs/specs/`, `docs/plan.md`, `BRIEF.md`,
`docs/backlog/`, `docs/review/journal.md`.
## Что из этого следует
**С9. Переезд healthlog:** `architecture.md` 1611 → обзор (поведение уезжает в
`openspec/specs` по разделу за задачу); `conventions.md`
`conventions/README.md`; `local-research.md` 1829 → `research/`; `plan.md`
`docs/tasks/PLAN.md`; `backlog/``docs/tasks/`; завести `docs/adr/`.
**С10. Переезд jellybit:** `BRIEF.md``docs/passport.md` (заодно обновить — не
трогался с 13 июня); `docs/specs/architecture.md``docs/architecture.md`;
`docs/specs/database.md``docs/database.md`; `docs/specs/jellyfin-layout.md`
`docs/research/`; `docs/specs/{recognition,review-ux,workflow}.md` сверить с
capability и удалить как дубли; `docs/review/journal.md`
`docs/review-journal.md`; `drafts/` растворить по H; `docs/backlog/`
`docs/tasks/`.
**С11. `adopt` меняет природу** — теперь он переносит файлы, а не правит
указатели. Разбирается в теме про старт проекта.
**С12. Открыто до [темы 6](06-docs-upkeep.md) (поддержание):** точная
формулировка триггера промоута в ADR; нужен ли механический `check` раскладки
документов, раз пути жёсткие; как не потерять остаток при постепенной чистке
`architecture.md`.
+115
View File
@@ -0,0 +1,115 @@
# 3. Брифа ревью нет — бриф это и есть канон (2026-08-03)
## Что было
Контракт брифа — 413 строк, 13 разделов, отдельный файл `docs/review-brief.md`,
который каждый проход читает как истину. Заполнение на обоих проектах дало 841 и
734 строки, и `REMAINING.md` уже отметил, что часть разделов вырождается в
пересказ.
Разбор по разделам после решения [Р6](02-project-doc-canon.md) (жёсткие пути)
показал: **посредник между агентом и файлом не нужен, когда путь известен**.
Восемь из тринадцати разделов дублируют канон или снимаются жёсткими путями.
## Решено
**Р9. Отдельного файла-брифа нет.** Проектную конкретику проходам дают документы
канона напрямую, по жёстким путям. Формулировка владельца: «артефакты в `docs` и
должны стать частями брифа, а для ревью достаточно дать ссылки на эти
артефакты».
*Причина:* один факт — один дом. Бриф был вторым домом для паспорта, инвариантов
и карты, а разошедшийся бриф хуже отсутствующего: он выглядит актуальным.
**Р10. Заводится `docs/security.md`.** Периметр **первой строкой** (целевой и
сегодняшний, если контур не развёрнут), недоверенный вход и его каналы, из чего
строятся пути и ключи, что разграничивает доступ, что чувствительнее чего, что
вне модели. Материал уже есть, но рассыпан: у healthlog — раздел
«Аутентификация» в `architecture.md` и строка про секреты в `CLAUDE.md`, у
jellybit — секреты в `conventions/config.md`. **Периметра нет ни у одного**, а
без него враждебный проход не выбирает между «открыт наружу» и «контур
доверенный».
**Р11. `review-journal.md``docs/review.md`:** журнал дефектов плюс настройка
конвейера под проект. Туда садится остаток брифа, который фактом о проекте не
является — типовые узлы, типовые ложноположительные, вопросы к проходам,
недоступно проверке.
*Причина:* все четыре — производные калибровки, и журнал им источник. `##
Вопросы к проходам` сам называет журнал главным источником; `### Перестали
проверять сознательно` требует ссылки на его запись.
**Р12. Журнал расширяется до всех воспроизведённых дефектов** с пометкой
«проскочил / пойман ревью». Проверочный набор для калибровки — выборка по
пометке.
*Причина:* пойманные дефекты с оракулом (768 МиБ пика, 5.019 с удержания
блокировки) сегодня не сохраняются нигде, кроме отчётов триажа в архиве change, а
они и есть лучшая опора для прохода — проектные, воспроизводимые, однажды
оказавшиеся правдой.
**Р13. Семантика гейта — в `CLAUDE.md`, расширением раздела «Команды».** Чем
краснеет безусловно и почему, где логи, что означает исход, чего в гейте
намеренно нет, **кто и когда обязан гонять дорогое вне гейта**, что запускать
запрещено (с путями). Гейт краснеет не только в ревью — это факт о проекте.
**Р14. Severity инвариантов дописывается в `CLAUDE.md`** рядом с формулировкой.
Контракт брифа сам называл это лучшим исходом; жёсткие пути делают возможным.
Оговорка «выведена по обратимости» исчезает вместе с пересказом.
## Канон после темы 3
```
CLAUDE.md что это, стек, инварианты с severity, команды,
семантика гейта, запреты, слоты
docs/
passport.md зачем и для кого; чем НЕ является; сценарии; референсы
architecture.md как сложено — обзор; окружение, внешние зависимости,
наблюдатель, характер потока
database.md схема хранилища; представление данных и настройки
с числовым значением (таймаут занятости, лимит тела,
режим журналирования, ретеншен)
security.md периметр первой строкой; недоверенный вход; из чего
строятся пути и ключи; разграничение; что вне модели
conventions/README.md + <тема>.md
research/README.md + <тема>.md наблюдения и измеренные числа с провенансом
adr/README.md + template.md + ADR-*.md
review.md настройка конвейера под проект + журнал дефектов
tasks/ av-dev-tasks
openspec/
config.yaml, specs/<capability>/spec.md, changes/archive/
```
Слотов **нет** у: `docs/review-brief.md`, `docs/drafts/`, `docs/specs/`,
`docs/plan.md`, `BRIEF.md`, `docs/backlog/`, `docs/review-journal.md`.
## Что из этого следует
**С13. Скилл `project-brief` растворяется.** Заведение недостающих документов
канона — часть скилла старта/адаптации ([тема 5](05-project-start-lifecycle.md),
требование [Т1](README.md)).
**С14. Девять charter'ов переписываются второй раз.** Сейчас каждый читает «из
раздела `## X` брифа»; станет — из файла канона. **Цена названа вслух:** первая
переписка (вынос в плагин) осталась незамеренной — `REMAINING.md`, пункт 1.
Вторая делает замер по четырём реальным находкам healthlog **обязательным, а не
желательным**: два неизмеренных изменения подряд в том самом месте, где
присваивается severity.
**С15. Теряется соседство фактов, и charter обязан сшивать.** Контракт
настаивал, что замер становится находкой только рядом с настройкой: «768 МиБ
пика» — аномалия, лишь если известно, что запись лежит сжатой и распаковывается
целиком; «5.019 с удержания блокировки» — отказ соседа, лишь если известен
таймаут занятости. Теперь это `research/` и `database.md`, и charter'ы `ops`,
`adversary`, `reimpl` обязаны прямо говорить «собери из этих двух», иначе проход
снимет верное число и честно понизит находку до гипотезы.
**С16. Деградация становится поразрядной** — и это лучше прежнего «нет брифа →
деградирует всё». Нет `security.md` — деградирует `adversary`; нет `research/`
числа неизвестны `ops`, `adversary` и `reimpl`; нет `passport.md`
архитектурный проход теряет границу домена. Каждый проход пишет свою строку в
границы покрытия.
**С17. Открытый вопрос из `REMAINING.md` закрыт:** раздел `## Триггеры`
удаляется вместе с брифом. Правило выбора профиля остаётся в скилле конвейера;
проектная конкретизация, если понадобится, — в `docs/review.md`.
+76
View File
@@ -0,0 +1,76 @@
# 4. Границы плагинов (2026-08-03)
## Что было
Связь `tasks``pipeline` уже сделана **ролями, а не именами**: скиллы говорят
«пайплайн проекта», «владелец спринта», «тот, кто ведёт задачи». Жёсткая ссылка
по имени ровно одна — `task-pipeline:112` на канонический текст правила про
остаток внутри `session`, и рядом обработан случай «плагин не подключён».
Слоты `CLAUDE.md` при этом дублировались уже внутри одного плагина: шесть у
`tasks`, семь у `session`, три пары — одно и то же. Темы 2–3 растворили ещё
часть: «куда переезжает суть» отвечает канон, «оракулы» — семантика гейта
(решение [Р13](03-review-brief-is-canon.md)), «где живёт разбор процесса» —
`docs/review.md` (решение [Р11](03-review-brief-is-canon.md)). Из тринадцати
остаётся около четырёх.
## Решено
**Р15. Три плагина: `av-dev-pm`, `av-dev-pipeline`, `av-dev-git`.**
- **`av-dev-pm`** (бывший `av-dev-tasks`) — управление продуктом: канон
документов, задачи, цели, спринты, старт и адаптация проекта. Владеет всем
`docs/`, включая `docs/tasks/`.
- **`av-dev-pipeline`** — исполнение: SDD-цикл, конвейер ревью, девять агентов.
- **`av-dev-git`** — стиль коммитов; работает в любом репозитории.
*Причина (словами владельца):* «пайплайн можно и переиспользовать в других
проектах с более простым подходом к управлению». Это подтверждается разбором:
пайплайн зависит от **файлов канона и от OpenSpec, а не от плагина** `av-dev-pm`.
В чужом проекте нужных файлов нет — включается поразрядная деградация (следствие
16), и это штатный режим, а не поломка.
*Имя:* `pm` = product management, «объединение всех операций по управлению
продуктом», и согласуется с `av-dev-git`.
**Р16. Граница «пайплайн не закрывает задачу» снимается.** Закрывает задачу и
двигает строки между `SPRINT.md` / `BACKLOG.md` / `REJECTED.md` **агент-
оркестратор** — `task-pipeline` и `task-batch`, а не сабагенты внутри них. Зовёт
он `tasks.py` через слот «Команда учёта задач» в `CLAUDE.md`.
Слот, следовательно, **не исчезает, а становится мостом между плагинами** — и
заодно тем, чего в чужом проекте нет, отчего пайплайн там работает как прежде:
докладывает исход, записей учёта не трогает.
**Р17. `av-dev-backlog` помечается устаревшим и остаётся** до перевода jellybit.
*(заменено на [тему 30](30-av-dev-backlog-removed.md): плагин удалён раньше
этого срока — условие пережило свою причину.)* Описание переписывается так,
чтобы не ловить триггер «добавь задачу в беклог» — иначе агент выбирает между
ним и `av-dev-pm` случайно.
## Что из этого следует
**С18. Переименование `av-dev-tasks``av-dev-pm`** тянет `plugin.json`,
`marketplace.json` и пространство имён скиллов: `av-dev-tasks:session`
`av-dev-pm:session`, включая ссылку из `task-pipeline:112`.
**С19. Раздел «Стимулы, которые процесс создаёт» в `session` переписывается.**
Снятая граница выбила механическую опору у трёх защит: «сжать задачу до
остатка», «занизить урожай», «занизить критерии приёмки» — во всех трёх приёмщик
и исполнитель теперь совпадают. Остаются: **отчёт триажа** в
`openspec/changes/<id>/review/` (независимый артефакт, `task-batch` уже сверяет
полноту ревью по нему, а не по прозе исполнителя), **`SPRINT.md` под git** с
видимой историей и **`reopen <slug> --reason`** — закрытие не окончательно,
приёмка человеком на сессии его отменяет. Раздел обязан назвать их поимённо,
иначе обещает защиту, которой нет.
**С20. Конфликт владения `docs/tasks/` снят** — канон и задачи теперь в одном
плагине.
**С21. Скилл `adopt` из `av-dev-tasks` поглощается** скиллом адаптации проекта
уровня канона (требование [Т1](README.md)). Разбирается в [теме
5](05-project-start-lifecycle.md).
**С22. Состав `av-dev-pm`:** `tasks`, `session` (есть), `docs` — ведение канона,
`project` — старт, adopt, check, upgrade ([тема
5](05-project-start-lifecycle.md)).
+89
View File
@@ -0,0 +1,89 @@
# 5. Старт проекта и жизненный цикл под каноном (2026-08-03)
## Что было
Требование [Т1](README.md): прийти в любой старый проект и перевести на текущие
рельсы; канон сам меняется, значит уже приведённые проекты тоже повышаются.
Существующий `adopt` (уровень задач) даёт готовую форму: **`scan` — только
чтение, карта → суждение человека → `apply` — запись одним проходом**, с отказом
до первой записи при неверной карте и с обязательным разделом «не разложилось»
поимённо. Форма переносится на уровень канона как есть.
Четыре операции различаются не поровну: `adopt`, `check` и `upgrade` — одна
машина сравнения с разными исходами, а `init` — принципиально другой режим,
разговор, а не сверка.
## Решено
**Р18. Два скилла: `av-dev-pm:init` и `av-dev-pm:canon`.** `init` — интервью по
входному брифу для нового проекта. `canon` — привести к канону: `check`,
`adopt`, `upgrade` одной машиной.
**Р19. Скелет канона заводится целиком, незаполненное называется пустым.** Все
файлы канона есть с первого дня, но незаполненный держит **одну честную
информативную строку**: «наблюдений на живых данных нет — внешний источник один,
формат документирован», «прецедентов не накоплено», «внешних зависимостей нет,
смотри на диск и на СУБД».
*Причина:* это тот же принцип, что был в контракте брифа, поднятый на уровень
файлов. Проход читает такую строку **как факт**, а не как пробел, и не тратит
обязательный вопрос впустую. Отсутствие файла он прочитать не может никак.
**Защита от вырождения в заглушки** берётся у `tasks.py`: пока на месте стоит
плейсхолдер шаблона, `check` о нём напоминает. Строка «TBD» — это не «пустое
названо пустым», и `check` обязан их различать.
**Р20. Скрипт `docs.py` плюс версия канона в `docs/.pm.json`.** Отдельный
скрипт, не расширение `tasks.py`: рефакторинг 2421 работающей строки ради
удобства вызова не окупается. `docs.py check` зовёт `tasks.py check` для своей
части.
**Граница механизируемого объявляется вслух — иначе `check` соврёт.**
| Проверяет `docs.py` | Судит агент |
| --- | --- |
| отсутствующие пути канона | смысловой дубль (`docs/specs/recognition.md` против capability) |
| файлы в `docs/` вне канона | поведение, оставшееся в `architecture.md` |
| битые относительные ссылки | протухший факт, разошедшийся с кодом |
| версия канона и её отставание | достаточность честной строки в пустом слоте |
| нетронутый плейсхолдер шаблона | |
`check`, отчитавшийся «канон соблюдён» на проекте, где из шести файлов три
лишние, хуже отсутствующего.
## Порядок интервью `init` — зависимость, а не удобство
Цель и потребители → чем это **не** является и мера успеха → периметр и что
недоверенное → стек, хранилище, необратимое → чем краснеет гейт → первые цели в
`PLAN.md`. Каждый блок опирается на ответ предыдущего.
Вход — свободный текст «что мне нужно и почему» (образец формы: `BRIEF.md`
jellybit, 6 КБ). После `init` его дом — `passport.md`; отдельным файлом он не
остаётся.
**`init` физически не производит полный канон.** В новом репозитории нет кода, а
`architecture.md`, `database.md`, `conventions/` и `research/` выводятся из него.
Они заводятся скелетом с честной строкой («архитектуры пока нет: кода нет,
заводится первой задачей») и наполняются шагом синка документации.
## Что из этого следует
**С23. `docs/.pm.json` поглощает `<tasks>/.tasks.json`.** Меняется цепочка
разрешения в `tasks.py` — сегодня он ищет `.tasks.json` вверх от текущего
каталога. Нужен переходный период либо чтение обоих.
**С24. `tasks.py adopt` становится шагом внутри `canon adopt`**, а не отдельной
пользовательской операцией: `docs/tasks/` — часть той же раскладки.
**С25. Версия канона — целое число**, не semver: у канона нет обратной
совместимости, есть только «приведён» и «не приведён».
**С26. Журнал изменений канона** живёт в плагине —
`av-dev-pm/skills/canon/references/changelog.md`, запись на версию: что
добавилось, что переехало, что удалено, что сделать проекту.
**С27. Открыто до [темы 6](06-docs-upkeep.md):** звать ли `docs.py check` из
гейта проекта. У healthlog `task gate` уже сверяет миграции с документацией, так
что место есть; но гейт принадлежит проекту, и плагин может только рекомендовать
строкой в отчёте.

Some files were not shown because too many files have changed in this diff Show More