Сабагент в роли продакт-менеджера оценил покрытие жизненного цикла личного проекта скиллами и агентами 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>
34 KiB
Канон документов проекта
Версия 4.
Это единственный дом определения канона. Скиллы init, canon и docs
читают его, а не пересказывают: три описания одной раскладки разъедутся, и
работать будет то, которое прочитали последним. Меняется канон — меняется этот
файл и появляется запись в changelog.md.
Зачем канон жёсткий
Пути фиксированы, и проект под них подгоняется, а не наоборот. Причина не техническая: проектов много, все малого и среднего размера, и ориентироваться в слегка похожих, но разных раскладках дороже, чем один раз привести их к общей. Рядом лежит OpenSpec, у которого структура тоже строгая.
Цена принята сознательно: плагин не переносится на чужой репозиторий как есть —
чужой репозиторий приводится к канону скиллом canon.
Раскладка отвечает, где текст лежит и на какой вопрос отвечает. Каким он должен быть словами — общий для всех документов канона файл language.md: информационный стиль, англицизмы, жаргон. Он относится и к задачам, и к решениям ADR, и к запискам разведки.
Сопровождение и эксплуатация — целое и часть
Одна тема живёт в трёх местах канона, и путать их слова нельзя.
Сопровождение — всё, чем держат проект: инструмент и сборка, процесс, выкладка, метрики и логи, инфраструктура, дежурство. Эксплуатация — его часть: работа системы на проде. Целое и часть, и никогда наоборот.
| Место | Уровень | Что там |
|---|---|---|
ROADMAP.md, секция Сопровождение |
план | работы, которые собираемся делать: цели и их задачи |
architecture.md, раздел «Эксплуатация» |
состояние | как устроено сейчас: где работает, что рядом, кто перезапускает |
| эксплуатационный проход ревью | оптика | чем проверяем: «это упало через неделю на проде» |
Слово «поддержка» не употребляется вовсе — в нём слышится помощь пользователю, а это другая работа.
Граница с возможностями проходит по тому, кто наблюдает. «Приложение сообщает о своём состоянии» — возможность приложения, её место среди прочих целей: наблюдает пользователь сервиса. «Дежурный видит состояние на одном экране» — сопровождение: наблюдаем мы. Одни и те же метрики попадают в разные секции роадмапа, и это верно — секции отвечают на разные вопросы.
Раскладка
CLAUDE.md памятка агенту: что это, стек, инварианты с
severity, команды, семантика гейта, запреты
docs/
.pm.json версия канона и пути, нужные проверкам
passport.md зачем и для кого; чем НЕ является; сценарии
architecture.md как сложено — обзор; окружение и эксплуатация
database.md схема хранилища; представление данных и настройки
security.md периметр; недоверенный вход; что вне модели
conventions/
README.md индекс, правило промоута, что механизировано
<slug>.md
research/
README.md как снималось, индекс
<slug>.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. Причина не эстетическая: имя файла стоит в ссылках из других документов, в коммитах и в путях, которые набирают руками, — а кириллица в пути ломается по-разному в разных местах и не набирается на английской раскладке.
Транслита не заводим. Слаг именуется английским словом по сути, а не
записью русского латиницей: queue-as-table, а не ochered-tablicej. Транслит
нечитаем тому, кто ищет по смыслу, и не сокращается.
У ADR имя вдобавок несёт форму — ADR-ГГГГ-ММ-ДД-slug.md: по ней записи
сортируются, и по ней же ищется дата решения.
docs.py check проверяет кириллицу и kebab-case жёстко, форму имени ADR —
тоже, а транслит эвристикой, то есть замечанием: английское слово от
транслита машина не отличает. Слаги каталога задач ведёт tasks.py — там та же
проверка и тот же разрез.
Переименование — не правка, а перенос ссылок: делается одним проходом по
всем местам, где имя упомянуто, иначе останутся битые ссылки. Для задач это
умеет tasks.py adopt; для документов канона правит человек, а docs.py потом
показывает, что ссылки целы.
Роли документов
Одна строка на каждый — на какой вопрос он отвечает и кто его читает.
| Документ | Вопрос | Кто читает, кроме человека |
|---|---|---|
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.
Заводится, когда верно одно из трёх:
- дорогой откат — переделка стоит дороже переписывания одного файла;
- намеренный отказ от очевидного подхода;
- пересмотр прежнего решения — тогда у старой записи обязателен статус «заменено на».
Не заводится для рутины и для того, что видно из кода и 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/, роадмап отвечает,
когда и в каком порядке оно появилось.
У каждой записи есть тип, и тип решает, что с ней можно делать. Дом типа —
поле меты Тип первой строкой; эмодзи в заголовке от него производна. Словарь
закрыт:
| Тип | Что это |
|---|---|
🎯 goal |
возможность приложения |
✨ feature |
снаружи появляется то, чего не было |
🐞 fix |
поведение расходится с заявленным |
🧹 chore |
обслуживание, поведение не меняется |
🔬 research |
исход — знание, а не изменение |
Схемы записи здесь нет намеренно. Какие разделы тип требует, нужна ли ему
цель и берётся ли он в спринт — скилл tasks: сводка в его
SKILL.md, раздел «Тип записи», подробно — по файлу на
тип в tasks/references/task-<тип>.md. Канон фиксирует словарь, потому что
от него зависит, читается ли проект как продукт; схема — механика ведения задач,
и второй её экземпляр разошёлся бы с первым (он и разошёлся: канон успел
объявить цель у fix запрещённой, хотя она там необязательна).
Схема требуется к взятию в спринт, а не к заведению: беклог пополняется чаще,
чем разбирается, и требование на входе выгоняло бы в заметки то, что должно
лежать задачей. Запись, не собравшая разделы своего типа, — законное состояние
беклога; невзятой её делает sprint take.
Отдельного типа для незаполненной записи нет: «ещё не описано» — состояние, а не
род работы, и называется оно research без раздела «Вопрос». Такая запись в
спринт не берётся и лежит в конце своей категории.
Раскладку, форму записи и алгоритм работы над каждым типом держит скилл tasks.
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/ |
идея → запись research; отказ → 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 | doc-consistency |
файлы в docs/ вне канона |
поведение, оставшееся в architecture.md |
doc-consistency |
| имя файла не kebab-case латиницей; форма имени ADR | транслит в имени — сверх эвристики | doc-wording |
| битые относительные ссылки | прямое противоречие между документами | doc-consistency |
| версия канона и её отставание | достаточность честной строки в пустом слоте | doc-consistency |
| нетронутый плейсхолдер шаблона | ADR без ссылки на design.md, замена без парного статуса |
doc-consistency |
| маркеры долга — числом | протухший факт, разошедшийся с кодом | doc-code-drift |
миграция изменена, а database.md нет |
зависимость в манифесте, не названная в обзоре | doc-code-drift |
capability без упоминания в architecture.md |
второй способ там, где обзор обещал единственный | doc-code-drift |
| связность и читаемость | doc-wording |
Агентов двое, и разведены они по глубине, а не по охвату. doc-consistency
читает только docs/ и openspec/, doc-code-drift — весь репозиторий и гоняет
читающие команды. Слитый агент делал бы одну половину поверхностной; тот же
разрез, что между task-form и doc-wording.
Зовутся оба одинаково — раз в спринт на сессии, а также после adopt и после
upgrade, на весь канон разом. Не на синке документации: агент на opus по
каждой сделанной задаче не окупается, а расхождение между двумя документами по
определению требует двух, и на большинстве задач синк правит один. Пачка,
отбираемая работой, вдобавок не видит того, чего работа не касалась, — а именно
там расхождение и живёт: правка отменяет решение в одном документе, парный статус
нужен в другом.
Перечень фактов, которые doc-code-drift сверяет с кодом, закрыт — имя
основной ветки, команды, пути, зависимости поимённо, настройки с числовым
значением, единые точки проекта, capability, проверяемые инварианты. «Сверить
архитектуру с кодом» задача без дна, и агент, которому её поставили, выдаёт
правдоподобную труху вместо находок.
docs/.pm.json
{
"canon": 4,
"migrations": "internal/store/migrations",
"tasks": {
"backlog": "INDEX.md"
}
}
canon — версия канона, под которую проект приведён, целым числом: обратной
совместимости у канона нет, есть «приведён» и «не приведён». migrations — путь
каталога миграций, если БД есть; по нему docs.py делает сверку с
database.md. tasks — настройки каталога задач, переехавшие сюда из прежнего
<tasks>/.tasks.json: один конфиг на весь канон, а не по одному на каталог.
Внутри tasks — только имена файлов и заголовков (items, backlog,
roadmap, sprint, rejected, sprint_section, oracle_word и заголовки
разделов тела: criteria_heading, surface_heading, questions_heading,
completion_heading, repro_heading, question_heading, answer_heading,
scope_heading), и ключ пишется, лишь когда имя отличается от умолчания.
Словаря типов здесь нет — он закрыт каноном, а не настраивается проектом:
настраиваемый словарь типов разъехался бы на синонимах ровно так же, как
открытый. Категорий беклога здесь тоже нет: их дом — заголовки ## самого
индекса, и второй список сразу разошёлся бы с первым. Неизвестный ключ tasks.py
отвергает кодом 3, поэтому лишнее слово в этом объекте останавливает работу с
задачами целиком.
Ключей будет больше по мере роста проверок; неизвестный ключ docs.py
игнорирует, отсутствующий — считает «проверка неприменима» и говорит об этом
строкой, а не молчит.