добавлены плагины av-dev-tasks и av-dev-pipeline

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

- av-dev-tasks — преемник av-dev-backlog: цели вместо приоритетов,
  спринт под одну цель с заморозкой набора, различение вопроса и
  блокера, каденция «вопросы — разбор — переоценка — набор».
  Раскладка docs/tasks с items/, PLAN.md, BACKLOG.md, SPRINT.md,
  REJECTED.md; проверенное из av-dev-backlog перенесено, не переписано.
- av-dev-pipeline — вынос того, что лежало копиями в healthlog и
  jellybit (3628 строк) и уже разошлось: цикл SDD, конвейер ревью с
  обязательным триажем, прогон нескольких задач разом. Проектная
  специфика вынесена в файл-бриф, charter'ы несут метод.

Коммит фиксирует состояние на момент ревью: три прохода нашли
блокирующие дефекты (нет шага, заводящего бриф; git rebase на занятой
worktree ветке; sprint drop пишет наполовину) — они чинятся следующими
коммитами. Сохранено как база, от которой видно правки.
This commit is contained in:
av
2026-08-03 11:01:29 +03:00
parent 092d07c15d
commit 9219f4a5cd
29 changed files with 5287 additions and 0 deletions
@@ -0,0 +1,413 @@
---
name: review-pipeline
description: Конвейер ревью изменения — детерминированный гейт, сверка с дельта-спеками в обе стороны, враждебные постановки и эксплуатационный постмортем, независимая реализация по триггеру, архитектура и обязательный триаж. Проходы гонятся последовательно; параллельно — только по явной просьбе и с явно названным набором. Проектная специфика приходит из файла-брифа. Вызывается из task-pipeline (чекпоинты ревью), из task-batch (финальная сверка) и отдельно — профилем design на предложении ДО кода.
---
# Конвейер ревью
Готовит ревью — **не заменяет его**. Потребитель отчёта — оркестратор, который
чинит код; человек читает только сводку, развилки и границы покрытия.
## Три правила, из которых всё следует
Если ситуация не покрыта инструкцией — решай по ним.
1. **Recall чек-листа равен длине чек-листа.** Проход, устроенный как «проверь
пункты 1..N», найдёт ровно перечисленное. Всё неявное — идиомы, форма
решения, «так не делают» — неперечислимо по определению: перечислимое уже
стало бы конвенцией. Отсюда деление проходов на **applicative** (применяют
заданный критерий) и **generative** (сперва порождают критерий или
альтернативу, потом сравнивают). Расширять чек-листы бесполезно; неявный слой
достают только generative-проходы.
2. **Ценность верификатора = наличие внешнего оракула × декорреляция с
автором**, а не число ролей. Под всеми ролями одна модель с одними
априорными, вход у всех общий: седьмая роль почти не добавляет recall, но
линейно удорожает триаж. Иерархия надёжности: детерминированный инструмент >
агент, который его **запускает** и интерпретирует вывод > агент с чистым
мнением. Максимум работы переносим вниз.
3. **Отчёт без границ покрытия хуже отсутствия отчёта.** «Критичных проблем не
обнаружено» потребляет ощущение проверенности, ничего не гарантируя. Секция
границ покрытия обязательна и не сокращается — в том числе в докладе человеку.
## Что конвейер защищает — приходит из брифа
Проходы общие, а нарушать нельзя проектное. Список инвариантов, команду гейта,
объёмы и модель угроз конвейер **не знает** — он читает их в брифе проекта:
[references/project-brief.md](references/project-brief.md) описывает контракт,
[references/brief-template.md](references/brief-template.md) — образец
заполнения.
Разреши путь к брифу один раз, в начале прогона: путь из задания →
`docs/review-brief.md``.claude/review-brief.md`. Дальше передавай готовым.
**Брифа нет — прогон идёт в деградированном режиме**: `critical` по основанию
«нарушен инвариант проекта» никем не присваивается, числа объёма не
используются, и в границы покрытия уезжает строка «брифа проекта нет». Это дыра
покрытия, а не нейтральное умолчание.
## Что получает каждый проход
Задание любому проходу состоит из шести вещей, и первые две без брифа
бессмысленны:
- **бриф** — путь;
- **контракт находок** — путь к
[references/finding-contract.md](references/finding-contract.md) (в
установленном плагине — `${CLAUDE_PLUGIN_ROOT}/skills/review-pipeline/references/`);
- **изменение** — идентификатор change и путь к его дельта-спекам;
- **база диффа**;
- **профиль и режим** прогона — чтобы проход знал, что писать в границы покрытия;
- **сужение**, если оно есть: конкретный узел, конкретная capability.
Чего проход **не** получает ни в каком режиме — выводов других проходов. См.
«Режим запуска».
## Модель по проходу
Следует из правила 2: чем больше работы делает детерминированный инструмент,
тем дешевле может быть модель; чем больше проход **порождает** критерий, тем
дороже. Модель задана во frontmatter каждого агента, менять её здесь не нужно.
| Модель | Проходы | Почему |
|---|---|---|
| `sonnet` | gate, code, ops | вход структурный, критерий записан заранее |
| `opus` | specs, adversary, rubric, reimpl | суждение без опоры на инструмент |
| `fable` | triage, architecture | ошибка распространяется дальше самой находки |
**Самая дорогая модель — только двум проходам, и это калибровка, а не
осторожность.** Замер: на первом же прогоне конвейера самые ценные находки дали
`opus`-проходы — сверка спек дала 13 находок с оракулами, а проход про
идиоматичность (впоследствии упразднённый) — три эксперимента против драйвера БД
с воспроизведёнными числами. Разницы в пользу более дорогой модели на
опиниативных проходах не обнаружилось — значит платить за неё там не за что.
Двое, у кого она остаётся, отобраны по одному признаку: **их ошибка
распространяется дальше собственной находки.**
- `triage` — через него проходит всё, что оркестратор реализует **молча**:
ложноположительная находка становится кодом, потерянный `critical` — дефектом.
Ошибка триажа дороже ошибки любого отдельного прохода.
- `architecture` — запускается редко (только `deep` и `design`), потолок в
3 находки делает его дешёвым по выходу, а находка на предложении стоит абзаца
против переписывания на готовом коде. Дёшево × высокое плечо.
`reimpl` намеренно **не** в этом списке, хотя он самый ценный из generative: его
стоимость определяется объёмом вывода (он пишет реализацию целиком), так что
дорогая модель множит самый большой счёт. Ценность же его — в **независимости**
взгляда, а не в мощности модели.
**Самая дешёвая модель не используется ни на одном проходе, и это не экономия
наоборот.** Дешёвая модель на опиниативном проходе даёт правдоподобные находки,
которые триаж обязан опровергать оракулом, — а это самая дорогая операция
конвейера. Механизируемая же работа здесь вынесена **ниже** модели: гейт,
покрытие диффа, карта проекта — это скрипты проекта, они стоят ноль токенов.
Дешёвому проходу просто не осталось работы.
Экономия достигается не понижением модели, а **непуском прохода**: `quick`
четыре прохода, `deep` — семь-восемь. Правило выбора профиля и есть главный
рычаг стоимости.
## Профили
| Профиль | Когда | Стадии | Проходов |
|---|---|---|---|
| `quick` | багфикс, локальная правка, доки | 0, 1, 5 | 4 |
| `standard` | новая функциональность в существующем пакете | 0, 1, 2, 5 | 6 |
| `deep` | новый пакет, изменение публичного контракта, миграция схемы, трогает инварианты брифа | 0, 1, 2, 3, 4, 5 | 78 |
| `design` | **до кода**, на предложении | specs + rubric + architecture (см. ниже) | 3 |
**Состав сверяется по этой таблице до коммита.** Реестр из трёх-восьми пунктов
проверяется взглядом — и это единственная защита от промаха, который уже
случился: пропуск прохода **не отличим от прохода без находок** (гейт зелёный,
спеки сошлись, отчёт выглядит полным), а заметить его мог бы только триаж,
который сам заполняется тем, что ему подали. Отчёт обязан перечислять запущенные
проходы **поимённо и с исходом**; непущенный идёт строкой «не запускался» в
границы покрытия, а не отсутствует. Цена молчащего пропуска измерена: семь
находок и отдельная задача на их дозакрытие.
Правило выбора профиля — **по факту изменения, не по ощущению важности**:
- есть миграция схемы, новый пакет, изменение публичного контракта (API,
протокол, формат на диске) или трогается правило, определяющее идентичность и
слияние данных → `deep`;
- иначе меняется поведение, видимое снаружи (эндпоинт, форма ответа, код ответа,
формат лога) → `standard`;
- иначе → `quick`.
Что именно в этом проекте считается публичным контрактом и какие пути означают
`deep` — раздел `## Триггеры` брифа. Он **уточняет** правило, а не отменяет его:
если триггеров в брифе нет, работает список выше.
Профиль объявляется в отчёте. Понижение профиля — решение оркестратора, и оно
попадает в границы покрытия строкой «профиль понижен до X, потому что …».
## Режим запуска: параллельно или последовательно
Профиль отвечает «какие проходы», режим — «как их запускать». Стадии всегда идут
по порядку номеров; выбор касается только проходов **внутри** стадии.
| Режим | Как | Когда |
|---|---|---|
| **последовательно** (умолчание) | по одному, следующий стартует после отчёта предыдущего | всегда, пока не попросили иначе |
| **параллельно** | названные проходы — одним сообщением | только по явной просьбе **и** с явно названным набором |
**Умолчание — последовательно, и его не надо обосновывать.** Обосновывается
отступление.
**Параллельный режим включается при двух условиях сразу**, и второе так же
обязательно, как первое:
1. **о нём попросили явно** — «гони параллельно», а не «сделай побыстрее»;
2. **названо, что именно гнать параллельно** — поимённый набор проходов
`specs` и `code` параллельно») или стадия целиком («стадию 1 параллельно»).
Просьба без набора — **не основание**: гоним последовательно и одной строкой
говорим, что набор не был назван. Это не придирка к формулировке. Параллелить
можно ровно то, что не мешает друг другу, а знание об этом лежит у того, кто
просит: он видит, занята ли машина, и ждёт ли он от прогона замеров. Домысливать
набор за него — значит принять решение, которое он оставил себе.
Почему умолчание именно такое:
- **Замеры.** Проходы `adversary` и `ops` доказывают находки числами: время
удержания блокировки, пик кучи, рост файлов журнала, длительность транзакции.
Два меряющих прохода на одной машине соревнуются за диск, CPU и за саму СУБД и
выдают числа, которые не воспроизведутся. Это не гипотеза: находки, ради
которых правило записано, опираются ровно на такие замеры (5.019 с удержания
блокировки при таймауте 5000 мс, пик 768 МиБ на теле 40 МиБ, 7 МБ/с роста
журнала, 1492 тика из 5502). Число, снятое под конкурентную нагрузку от
соседнего прохода, — это находка с испорченным оракулом, а её опровержение
стоит дороже всего выигрыша от параллельности.
- **Машина одна.** Рядом идёт задача, поднят сервис, гоняется гейт или дорогая
проверка проекта.
- **Ранний выход** возможен только при последовательном прогоне (см. ниже).
- **Разбор самого конвейера.** Когда выясняется, почему проход чего-то не нашёл,
порядок и изоляция важнее скорости.
Если параллельный режим всё же включён, в границы покрытия идёт строка: какие
проходы шли разом и что замеры, снятые в этом прогоне, как оракул слабее.
**Чего режим не меняет — и это не подлежит обсуждению.** Проход **не видит**
находок других проходов ни в каком режиме. «Последовательно» значит «по
очереди», а не «следующий читает предыдущего». Вся ценность конвейера держится
на декорреляции: под всеми ролями одна модель с одними априорными, и стоит
показать ей чужой вывод — она согласится. Согласие нескольких проходов и так не
повышает `confidence` (см. «Честный предел»); согласие **наведённое** ещё и
маскируется под независимое подтверждение. Единственный, кто видит всё, — триаж,
и это его работа.
**Ранний выход** (последовательный режим делает его возможным — это его побочная
выгода, а не повод его выбирать). Допустимо остановить прогон, не докатив
остаток, ровно в одном случае: находка требует **переделки формы** изменения, и
остальные проходы будут смотреть на код, которого через час не станет. Тогда:
- прогон останавливается, находка чинится, конвейер запускается **заново с
нулевой стадии** — а не «доезжает» остатком по старому коду;
- незапущенные проходы идут в границы покрытия строкой «не запускался: прогон
остановлен на <проход> из-за <находка>», поимённо;
- триаж запускается только на полном прогоне. Отчёт триажа по половине проходов —
ровно тот случай, который уже стоил семи находок: он выглядит полным, потому
что агрегирует всё, что ему подали.
Ранний выход по находке, которая чинится в пределах существующей формы
(`Действие: инлайн`), **не делается**: дешевле дособрать все находки и починить
пачкой, чем гонять конвейер дважды.
Режим объявляется в отчёте наравне с профилем, и если он **параллельный**с
причиной и составом. Последовательный объявляется одним словом.
## Стадия 0 — Gate (обязательна во всех профилях)
Агент `review-gate`. Запускает команду гейта из раздела `## Гейт` брифа и
интерпретирует вывод.
**Пока гейт красный — опиниативные проходы не запускаются.** Оркестратор чинит и
перезапускает гейт. Исключение одно: отказ, унаследованный от базовой ветки
(гейт проверяет это прогоном на базе) — тогда он фиксируется находкой и не
блокирует.
Гейт возвращает не только «зелено/красно», но и находки класса **отсутствующая
верификация**: изменённые строки без покрытия, конкурентность без теста с
параллельным доступом, флаки-тест (не ниже `major`), недоступный инструмент.
Шаги выбираются по изменённым файлам: правка документации не гоняет тесты,
линтеры и детектор гонок. Пропуск при этом не молчит — он виден в сводке с
причиной и уезжает в границы покрытия, как и любой другой `SKIP`.
Шаги, которые красят гейт безусловно, перечислены в брифе с причиной. Проходу
запрещено списывать такой отказ в мелочь.
## Стадия 1 — Conformance (обязательна во всех профилях)
Два applicative-прохода: оба применяют **записанный** критерий, оба дешёвые.
Замеров они не делают и потому безобиднее прочих, если параллельный режим
попросят с их именами; сами по себе идут по очереди, как и все.
- `review-specs` — критерий взят из **дельта-спек предлагаемого изменения**, а не
из proposal, сообщения коммита или описания задачи. Сверка двунаправленная;
направление `code → spec` важнее.
- `review-code` — критерий взят из файла конвенций проекта (раздел `## Карта`
брифа), и только та его часть, которая **не выражается правилом**:
механизируемое уже проверила стадия 0. Что именно механизировано, тот же раздел
брифа перечисляет — повторять это проходом вредно.
Recall обоих равен длине их источника — это и есть предел applicative-проходов,
ради которого существует стадия 2.
## Стадия 2 — Adversarial и operational (`standard`, `deep`)
Два прохода:
- `review-adversary` — находка есть **построенный путь**, а не свойство;
- `review-ops` — постмортем от симптома у владельца сервиса к строке кода.
**Эту пару параллелить не стоит даже по просьбе — переспроси.** Оба доказывают
находки замером, и оба меряют одно и то же железо. Запущенные разом, они портят
числа друг другу, а испорченный оракул хуже отсутствующего: находка выглядит
доказанной. Если их всё же назвали в параллельном наборе — выполняй, но скажи в
границах покрытия, что числа этого прогона сняты под соседней нагрузкой.
**Эта стадия зарабатывает больше всех остальных вместе, и потому стоит в
`standard`, а не только в `deep`.** Измерено на пяти задачах подряд: враждебный
проход дал пять из семи выживших находок дозапуска (включая обе верхние);
эксплуатационный — единственный, кто нашёл, что откат бинаря поверх новой схемы
стартует молча. Оба несут внешний оракул по построению: один обязан путь
**прогнать**, второй смотрит ось времени и эксплуатации, которую не смотрит
никто другой.
Материал обоим даёт бриф: `## Модель угроз` — враждебному, `## Прод и поток`
эксплуатационному. Без этих разделов стадия вырождается в общие места.
## Стадия 3 — Independent reimplementation (`deep`, по триггеру)
- `review-reimpl` — пишет свою реализацию, не открывая существующую, затем
диффит по решениям. **Запускается по триггеру, а не всегда:** изменение вводит
новое правило идентичности, слияния или разбора (проектная формулировка
триггера — в разделе `## Триггеры` брифа). Это самый дорогой проход конвейера
(его счёт определяется объёмом вывода — он пишет реализацию целиком), а вне
этого триггера независимый взгляд в значительной мере уже дал профиль `design`:
код писался под его находки. Триггер выбран по факту: единственный раз, когда
триаж назвал отсутствие `reimpl` дырой покрытия, — это была задача с новым
правилом слияния сущностей.
## Стадия 4 — Global (`deep`, `design`)
Агент `review-architecture`. Получает **вход шире диффа**: дерево пакетов с
назначением, граф внутренних зависимостей, инвентарь существующих концепций.
Команду, которая это готовит, даёт раздел `## Команды` брифа; нет команды —
проход собирает карту сам и говорит об этом в границах покрытия.
Главный вопрос — концептуальная целостность и **второй способ** делать то, что
уже делается. Он же и оправдывает проход: на задаче про пересборку архитектурный
проход нашёл, что новый код был **вторым проигрывателем журнала** со своим
порядком. Второй обязательный вопрос — **что опытный человек отсюда удалил бы**:
слой с единственной реализацией, интерфейс ради мока, незапрошенная
конфигурируемость, подстраховка поверх подстраховки. Потолок — 3 находки плюс
секция «дешевле переделать до мерджа».
## Стадия 5 — Triage (обязательна)
Агент `review-triage`. Единственный, кто агрегирует. Получает сырые выводы всех
проходов, `git diff`, профиль, режим и **список запущенных проходов**; возвращает
финальный отчёт.
Без триажа проходы дают порядка сорока замечаний при единицах существенных.
Потребитель здесь — оркестратор, который **молча реализует** всё, что прочитал:
цена нетриажированного отчёта — не потерянное время человека, а разросшийся от
вкусовщины код.
Порядок: дедупликация по причине → оракул для всего `critical`/`major`
понижение неподтверждённого до гипотезы → отсев вкусовщины → ранжирование по
ущербу × вероятности → потолок 7 пунктов в основном списке.
## Профиль `design` — до кода
Запускается на шаге ревью спек (шаг 4 скилла `task-pipeline`), когда change уже
имеет `proposal.md` и дельта-спеки, но кода ещё нет. Состав:
1. `review-specs` в режиме «дизайн ДО кода»;
2. `review-rubric`, фаза 1 без фазы 2: рубрика на задуманный узел становится
приёмочными критериями и уезжает в `tasks.md`;
3. `review-architecture` на предложении: вводит ли change новое понятие, можно ли
выразить существующими — **включая конструкции стандартной библиотеки**, — не
появляется ли второй способ. Вопрос «не изобретаем ли то, что уже есть в
библиотеке» живёт здесь;
4. вопрос автору дизайна: **«предложи три формы решения и назови компромисс
каждой»** — если ответ показывает, что рассматривалась одна, это находка.
Смысл профиля: архитектурная находка на готовом коде стоит переписывания и
поэтому игнорируется; та же находка на предложении стоит абзаца обсуждения.
`rubric` живёт **только** в этом профиле. Судить код по критерию, под который он
писался, — корреляция по построению; те же 8–14 свойств уже лежат приёмочными
критериями в `tasks.md`.
## Контракт находок
Единый для всех проходов — [references/finding-contract.md](references/finding-contract.md).
Коротко: заголовок через **последствие**, обязательные поля `Файл`, `Severity`,
`Confidence`, `Оракул`, `Последствие`, `Предложение`, `Найдено проходом`.
`critical` без оракула или построенного пути не существует. Находка без поля
«Последствие» не выводится вовсе.
Каждый проход завершает вывод блоком `## Coverage of this pass`.
## Что происходит с находками дальше
- Оркестратор чинит помеченное `Действие: инлайн` и **не логирует мелочь**.
- `Действие: развилка` — вопросом с вариантами и ценой каждого туда, где проект
держит вопросы (это знает вызвавший пайплайн, а не конвейер). Оркестратор не
останавливается: он урезает изменение до остатка и доводит его.
- Находка не для этого мерджа, но реальная (отложенный `major`, развилка,
решённая «потом») — не теряется: заводится задачей средствами проекта, с
оракулом и провенансом в теле. Мелочь класса `nit` — пачкой, а не записью на
находку.
- `Promote candidates` — по процедуре [references/promote.md](references/promote.md):
находка → конвенция → правило линтера → **удаление из конвенций и из брифа**.
Третий шаг обязателен.
- Дефект, проскочивший ревью и всплывший позже, идёт в журнал проекта
([references/review-journal.md](references/review-journal.md)) — сразу, не
ретроспективно: теряется именно причина непоймания.
- **Отчёт триажа сохраняется вместе с изменением** (например, в
`openspec/changes/<id>/review/`). Он единственное, по чему потом видно, что
было найдено и что из этого не заведено: нулевой урожай при непустом отчёте
виден сразу.
## Честный предел
Модель воспроизводит медиану публичного кода, смещённую к популярному и
туториальному: отсюда тяга к интерфейсам ради интерфейсов, лишним мокам и
конфигурируемости, которую никто не просил. **«Идиоматично» и «распространено» —
разные вещи**; проходы обязаны различать их и опираться на поимённое положение
гайда, а не на ощущение частотности.
Согласие нескольких проходов — **не подтверждение**: это один источник,
высказавшийся несколько раз. Совпадение повышает приоритет, но не `confidence`.
Что недоступно **этому** проекту принципиально — перечисляет раздел
`## Недоступно проверке` брифа, и он целиком уезжает в границы покрытия.
Независимо от проекта недоступно:
- поведение внешних систем в их будущих версиях;
- реальный профиль нагрузки и то, что на самом деле лежит в данных;
- завязка внешних потребителей на текущую форму ответа;
- суждение «этой функциональности не должно существовать».
Отдельно и честно: **поимённая сверка с положениями стайлгайдов языка не
задаётся ни одним проходом.** Проход про идиоматичность упразднён, его способные
части переселены (эксперимент против поведения библиотеки и драйвера — в `ops`,
вопрос 8; «не изобретаем ли то, что уже есть в библиотеке» — в `architecture`,
вопрос 1), но различение «идиоматично против распространено» теперь не спрашивает
никто. Класс обратимый — портит форму кода, не данные, — и его надо признавать в
границах покрытия, а не считать проверенным.
Это и есть причина, по которой конвейер готовит ревью, а не заменяет его.
## Ссылки
- [references/project-brief.md](references/project-brief.md) — контракт брифа проекта.
- [references/brief-template.md](references/brief-template.md) — шаблон брифа.
- [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) — журнал проскочивших дефектов.
@@ -0,0 +1,179 @@
# Шаблон брифа проекта
Скопируй в `docs/review-brief.md` и заполни. Контракт разделов — в
[project-brief.md](project-brief.md); здесь только образец заполнения.
Курсивом даны пояснения — их из готового брифа убирают. Примеры взяты из двух
разных проектов (коллектор данных с непрерывным потоком и связующий сервис вокруг
внешних демонов), чтобы было видно, как один и тот же раздел выглядит при разной
природе проекта.
---
## Проект
*Абзац: что делает — и чего не делает.*
> Коллектор выгрузок с телефона. Принимает доставки, хранит их и отдаёт другим
> сервисам. Это **хранилище, а не аналитика**: принять, дедуплицировать,
> сохранить, отдать. Не переименовывать поля источника, не интерпретировать
> значения; свёртка считается только в ответе на запрос.
> Связующий сервис между качалкой и медиасервером: принимает задание, качает,
> распознаёт содержимое, раскладывает файлы ссылками. **Не медиатека и не
> плеер** — ничего не хранит сверх метаданных о раскладке.
## Инварианты
*Проверяемое свойство + последствие + severity по умолчанию. Цитируются
формулировкой.*
- **Точка сохраняется дословно.** Незнакомое поле не отбрасывается, число не
округляется при записи. Нарушение — необратимая потеря: сырой архив живёт
14 дней, дальше истина только в свёртке. По умолчанию `critical`.
- **Источник неприкосновенен.** Только `mkdir`/`link(2)`/`unlink` собственных
ссылок; файлы под каталогом загрузок не трогаются никогда. Нарушение —
повреждение чужих данных, необратимое. По умолчанию `critical`.
- **Сохранили — значит приняли.** Код ответа отражает доставку, а не разбор:
непонятое содержимое — `200`, тело уже на диске. Нарушение стоит доставки,
которую отправитель не повторит. По умолчанию `critical`.
- **Секреты и данные пользователя не в логах.** Тело запроса — только на `DEBUG`
и с обрезкой. По умолчанию `critical`.
- **Агрегации при записи нет.** Нарушение искажает историю молча и
диагностируется только сверкой с внешним источником, то есть месяцами позже.
По умолчанию `major`, `critical` — если испорченное невосстановимо.
## Гейт
- **Команда:** `task gate BASE=<база>`; база по умолчанию —
`git merge-base HEAD master`, на `master``HEAD~1`.
- **Логи шагов:** `tmp/gate/<шаг>.log`. Сводка печатает `OK`/`FAIL`/`WARN`/`SKIP`;
краснит гейт только `FAIL`.
- **Шаги:** сборка, `vet`, линтеры, форматирование, тесты, повторный прогон на
флаки, `-race`, покрытие изменённых строк, накат миграций с нуля, поиск
секретов, `govulncheck`.
- **Красят безусловно** *(перечислить с причиной — это главная часть раздела)*:
- `no-user-data` — файл из каталога данных попал под контроль версий: убрать
обычным коммитом уже нельзя;
- `config-samples` — структура конфига изменилась, а образец нет: забытое поле
обнаруживается не тестом, а тем, что через полгода о нём никто не знает;
- `migrations` — миграции не накатываются с нуля: восстановление перестаёт
работать ровно тогда, когда оно нужно;
- `er-schema` — миграция тронута, а схема в документации не обновлена.
- **Чего в гейте намеренно нет:** прогон на живом корпусе (`task verify:archive`)
— минута работы и данные, которых нет ни на какой другой машине. У этой
проверки краснота не видна никому до следующей задачи, которая до неё
дотянется, — говори об этом в границах покрытия.
## Команды
- Карта проекта для архитектурного прохода: `task review:context > tmp/review-context.md`
- Поднять изменение вживую: `task restart`, логи — `task logs`
- Тесты и линт: `task test`, `task lint`
- Дорогое, вручную: `task verify:archive` (минута, живые данные)
- **Запускать запрещено:** ничего, что пишет в `./data`, в рабочую БД и в боевой
каталог архива. Замеры — только на копиях в `./tmp`.
## Прод и поток
- **Где:** один статический бинарь в контейнере на домашнем сервере, перед ним
обратный прокси с TLS, SQLite на диске. Ни оркестратора, ни реплик, ни дежурной
смены.
- **Внешние зависимости и как каждая отказывает:** прокси — рвёт соединение на
длинном теле; диск — заполняется и тормозит; СУБД — отдаёт «занято» под
параллельной записью; приложение-источник на телефоне — молча перестаёт слать.
*(В другом проекте здесь были бы качалка, медиасервер, LLM и база метаданных, и
каждая — со своим «отвечает медленно», а не только «упала».)*
- **Кто заметит отказ:** один пользователь-владелец, в лучшем случае вечером, а
скорее не заметит вовсе.
- **Характер потока:** телефон шлёт непрерывно и молча; обратной связи у
отправителя нет, об отказах он не сообщает, расписание плавает. Тихо
сломавшаяся доставка — главный эксплуатационный риск.
- **Числа (с провенансом):** нижний слой — порядка 135 тыс. точек в сутки
(замер, `docs/local-research.md`); тела доходили до 42 МБ (там же); запись —
read-modify-write под конкурентными доставками (`docs/architecture.md`).
- **Обратимость:** падение сервиса обратимо — отправитель дошлёт широким
проходом. Потеря или порча точки необратима. Поэтому тихая порча весит больше,
чем «сервис вернул 500».
## Модель угроз
- **Недоверенное:** тело доставки целиком (имена метрик, единицы, формы точек,
метки времени, глубина вложенности, размер); заголовки доставки, часть которых
участвует в решениях; содержимое архива внешнего экспорта (имена файлов внутри
zip мы не формировали); параметры читающего API.
- **Из чего строятся пути и ключи:** файл сырого архива —
`raw/ГГГГ/ММ/ДД/<ulid>.json.gz`, дата берётся из времени приёма, имя — из
генератора идентификаторов; ключ записи — `метрика + слой + начало + конец`,
источник в ключ не входит.
- **Разграничение:** статические токены в `Authorization: Bearer`, раздельные на
запись и на чтение; конфиг под `0600`.
- **Что дороже:** данные пользователя дороже токена. Путь, по которому значение
доезжает до лога выше `DEBUG`, до ответа с ошибкой или до `testdata` в git, —
полноценная находка, а не замечание по гигиене.
- **Вне модели:** злоумышленник в локальной сети; вредоносный оператор;
компрометация поставщика данных; мультиарендность. Находки этих классов не
выводятся — они никогда не будут исправлены.
## Карта
- Актуальные спеки: `openspec/specs/<capability>/spec.md`
- Дельта-спеки изменения: `openspec/changes/<id>/specs/*/spec.md`
- Конвенции прозой: `docs/conventions.md`. Механизировано и потому **не
проверяется проходом по конвенциям**: форма логов, `fmt.Print*`/`os.Getenv`/
`time.Now` мимо единых точек, сравнение ошибок, сторонние пакеты ошибок —
всё это правила в `.golangci.yml`.
- Архитектура и решения: `docs/architecture.md`
- Журнал проскочивших дефектов: `docs/review-journal.md`
- **Единые точки:** идентификаторы — `internal/ident`; время — `store.Now()`;
разбор дат входного формата — один парсер в `internal/parse`; маппинг доменной
ошибки в код ответа — одна точка в `internal/httpapi`; путь приёма — `ingest`,
общий для HTTP и CLI. Инвентарь целиком выгружает `task review:context`.
- **Нумерованные артефакты:** миграции — `internal/store/migrations/NNNN_*.sql`,
номер монотонный, следующий свободный смотреть там же.
- Задачи: `docs/backlog/` *(пайплайн только читает и сообщает исход)*
- Реальные пакеты для тестов разбора: `internal/parse/testdata` — там данные
пользователя с вычищенными токенами, наружу не копировать
- Временное: `./tmp` (не системный `/tmp`)
- **Не трогать:** `./data` — боевой архив и БД
## Типовые узлы
*Род узла + 3–5 специфичных проверяемых свойств.*
- **Разбор входного формата** — поведение на усечённом и враждебном входе,
границы размера, отсутствие паники, детерминизм, судьба незнакомых полей.
- **HTTP-обработчик приёма** — валидация формы конверта до записи, лимит тела и
архивная бомба, что попадает в ответ, а что в лог, отсутствие доменной логики
в транспорте.
- **Обработчик читающего API** — предсказуемость размера ответа, поведение при
пустом диапазоне, коды ответа на невозможный запрос.
- **Репозиторий** — границы транзакции, конкурентная запись того же ключа,
откуда берутся время и id, что возвращается при отсутствии записи,
идемпотентность повторной записи.
- **Файловое хранилище с ретеншеном** — атомарность записи, поведение при
неполной записи и нехватке места, что удаляется и по какому критерию, можно ли
удалить лишнее.
- **CLI-команда пересборки** — идемпотентность повторного прогона, поведение при
отмене на середине, что остаётся после падения, отчёт для человека.
- **Клиент внешнего сервиса** — таймаут, протяжка `context`, поведение при
«медленно» против «упало», ретраи и их граница.
## Триггеры
- `deep`: миграция в `internal/store/migrations/`, новый пакет `internal/*`,
изменение контракта читающего API, правило слияния или вывод слоя.
- «Видимое снаружи» (то есть `standard`): эндпоинт, форма ответа, код ответа
приёма, формат лога.
- `reimpl` запускается, когда изменение вводит **новое правило слияния,
идентичности или разбора**.
## Недоступно проверке
- Поведение внешнего приложения-источника на следующем его обновлении.
- Что реально лежит в системе-источнике: сверить можно только ручным экспортом,
а он делается раз в 2–3 месяца.
- Поведение таблицы под объёмом нескольких лет истории и реальный профиль
нагрузки.
- Завязка внешних потребителей на текущую форму ответа.
- Суждение «этой функциональности не должно существовать».
@@ -0,0 +1,85 @@
# Калибровка проходов
Без измерения набор проходов растёт монотонно и вырождается в театр: каждый
кажется полезным, потому что иногда что-то говорит. Калибровка отвечает на
единственный вопрос — **ловит ли проход дефект своего класса**.
## Процедура (инъекция дефекта)
1. Взять **реальный коммит** из истории (`git log --oneline`), лучше
архивированный change с непустым диффом.
2. Внести в него **один** дефект того класса, который проход обязан ловить по
своему charter'у. Дефект должен быть правдоподобным — таким, какой реально
пишет модель, а не карикатурой (`panic("TODO")` не считается).
3. Прогнать **только этот проход** на подготовленном диффе — **три раза**,
каждый в чистом контексте.
4. Зафиксировать: нашёл `n/3`, число находок всего, число ложных.
5. Вердикт:
| Результат | Вердикт | Что делаем |
|---|---|---|
| нашёл 3/3 или 2/3, ложных немного | `keep` | ничего |
| нашёл 1/3 или 0/3 | `retune` | правим charter — сужаем вход, убираем чек-лист, добавляем оракул |
| `retune` уже был дважды подряд | `drop` | удаляем проход |
| находит, но ложных больше трети от всех находок | `retune` | триаж съедает больше, чем экономит проход |
**`retune` не более двух раз подряд.** Проход, не находящий дефект своего класса
в 2 из 3 прогонов после двух правок промпта, — это театр. Удалять, а не
бесконечно править формулировки: каждая итерация правки промпта стоит дороже,
чем отсутствие прохода.
**Существующий проход не удаляется без замера.** Сначала калибровка, потом
решение — иначе удаляется то, что работало, а остаётся то, что громче. Обратный
пример уже был: проход про идиоматичность стоял в списке на удаление как
«вкусовщина», а замер показал, что он зарабатывает **экспериментами против
поведения библиотеки и драйвера**, — и находка, воспроизведённая числом, отменила
решение, принятое по ощущению.
## Состав проходов принадлежит плагину, а не проекту
Проходы общие. Проект не может удалить проход — он может **не звать** его, и
тогда это идёт строкой «не запускался» в границы покрытия, как любой другой
пропуск. Молча сузить состав нельзя: пропуск прохода не отличим от прохода без
находок.
Отсюда два следствия:
- **правка charter'а — правка для всех проектов.** Прежде чем сужать
формулировку под свою боль, проверь, не место ли ей в брифе: предмет проверки
живёт там, метод — в charter'е;
- **удаление прохода из плагина требует замера на двух проектах**, а не на одном:
класс, не всплывший здесь, мог быть единственным работающим там.
## Пробы дефектов по проходам
Проба — заготовка инъекции. Список пополняется из журнала проскочивших дефектов
(см. [review-journal.md](review-journal.md)): реальный проскочивший дефект —
лучшая проба, какая вообще возможна, потому что синтетические смещены в сторону
тех, которые уже умеешь придумывать.
| Проход | Класс дефекта для инъекции | Заготовка пробы |
|---|---|---|
| `review-gate` | отсутствующая верификация | убрать тест на изменённую ветку, оставить код рабочим |
| `review-specs` | поведение вне спеки | добавить незаказанный фолбэк-дефолт на пустом входе |
| `review-code` | нарушение прозаической конвенции | увести штатный отказ мимо единой точки трансляции ошибки |
| `review-rubric` | нарушенное свойство узла | у клиента внешнего сервиса убрать таймаут и протяжку `context` |
| `review-reimpl` | форма решения | размазать решение по трём слоям там, где хватало одной функции |
| `review-architecture` | второй способ | завести вторую точку генерации id мимо единой |
| `review-adversary` | построенный путь | принять внешний идентификатор без разбора до запроса в хранилище |
| `review-ops` | деградация окружения | убрать обработку недоступности внешней зависимости в фоновом цикле |
| `review-triage` | шум | подать 20 находок, из них 15 вкусовщина и 3 дубля — проверить потолок и дедуп |
Метрик сверх этого не заводим. Precision, корреляция между проходами, стоимость
прогона в токенах — всё это красиво звучит и никем не считается вручную; набор
показателей, который не собирают, создаёт впечатление измеряемости и тем вреден.
Работает ровно один механизм: инъекция дефекта и вердикт. Если корреляция двух
проходов действительно бросается в глаза — это видно по полю `Найдено проходом`
в триажированных отчётах и без отдельной метрики.
## Когда калибровать
- при заведении нового прохода — **до** включения в профиль по умолчанию;
- при правке charter'а существующего — иначе непонятно, правка помогла или нет;
- при появлении записи в журнале проскочивших дефектов — калибруем тот проход,
который должен был поймать;
- планово — нет. Календарная калибровка ради галочки сама превращается в театр.
@@ -0,0 +1,97 @@
# Контракт находок
Единый формат для всех проходов конвейера ревью. Проход, нарушивший контракт,
считается сломанным — триаж вправе выбросить его вывод целиком.
## Форма находки
```
### <краткая формулировка ПОСЛЕДСТВИЯ, не симптома>
- Файл: internal/<пакет>/<файл>.go:120-134
- Severity: critical | major | minor | nit
- Confidence: high | medium | low
- Оракул: <падающий тест / команда с выводом / положение гайда / нет>
- Последствие: <что произойдёт и при каких условиях>
- Предложение: <конкретное изменение>
- Найдено проходом: <имя агента>
```
## Правила
- **Заголовок через последствие.** Не «нет проверки токена», а «читатель без
токена выгрузит всю историю». Не «слияние перезаписывает запись», а «повторная
доставка сотрёт поля у уже сохранённой записи, и восстановить их нечем».
Симптом в заголовке — это заявка на то, что читатель сам достроит последствие;
он не достроит, он просто починит симптом.
- **`critical` без оракула или построенного пути не существует.** Оракул — это
падающий тест, вывод выполненной команды или поимённое положение гайда. Не
«вероятно, здесь гонка», а прогон детектора гонок с его выводом.
- **`confidence: low` — это «так обычно пишут».** Такие находки допустимы, но не
поднимаются выше `minor`. Частотность конструкции в публичном коде — не
аргумент.
- **Находка без поля «Последствие» не выводится вовсе.** Пустое «Последствие:
ухудшает читаемость» равносильно отсутствию поля.
- **`nit` допустим только при нарушении записанной конвенции** — со ссылкой на
файл и раздел конвенций проекта (путь — из раздела `## Карта` брифа) либо на
правило линтера. Если правило механизируемо, но не механизировано — это не
находка ревью, это `Promote candidate` (см. [promote.md](promote.md)).
- **`critical` по основанию «нарушен инвариант проекта» требует брифа.** Ссылка
идёт на пункт раздела `## Инварианты` дословно. Без брифа такое основание
недоступно — см. [project-brief.md](project-brief.md), деградированный режим.
- **Расхождение — не дефект, пока не названо последствие.** Особенно для прохода
независимой реализации: «я бы сделал иначе» без последствия не выводится.
## Шкала severity
| Severity | Что это | Пример |
|---|---|---|
| `critical` | нарушение инварианта проекта, потеря или порча данных, утечка секрета, построенный путь к отказу | запись потеряна при слиянии; тело пользовательской выгрузки в поле лога |
| `major` | сломанное требование дельта-спеки, необрабатываемый отказ штатного сценария, флаки-тест, поведение вне спеки, меняющее исход | приём отвечает 200, не записав тело: доставка считается принятой, а данных нет |
| `minor` | отступление от конвенции с реальной ценой, отсутствующая наблюдаемость, дублирование, которое разойдётся | ни одного чекпоинта на пути разбора: молчащая автоматизация неотличима от пустого потока |
| `nit` | нарушение записанной конвенции без последствий за пределами чтения | `msg` с интерполяцией вместо константы |
Шкала привязана к обратимости, а не к громкости: класс «необратимо и молча»
всегда весит больше класса «шумно и лечится повтором». Что здесь необратимо,
говорит раздел `## Прод и поток` брифа.
## Блок границ покрытия
Каждый проход завершает вывод этим блоком. Он не сокращается и не заменяется
фразой «всё проверено».
```
## Coverage of this pass
- проверено: <что реально прочитано/запущено, с путями и командами>
- не проверялось и почему: <бюджет, недоступный инструмент, вне входа>
- принципиально недоступно этому проходу: <из charter'а агента>
```
## Финальный отчёт триажа
Секции строго в этом порядке, потолок — 7 пунктов в первых двух:
1. `Блокирует мердж` (≤3, каждая с оракулом);
2. `Стоит исправить сейчас` (≤4);
3. `Гипотезы без доказательства` — что понижено и почему;
4. `Promote candidates` — кандидаты в конвенцию или правило линтера;
5. `Границы покрытия` — сводная, обязательная.
Перед секциями — сводка для человека: профиль и режим прогона, состояние гейта,
**перечень запущенных проходов поимённо с исходом каждого**, сколько находок
пришло на вход и сколько осталось. Перечень обязателен: пропуск прохода не
отличим от прохода без находок, и назвать его больше некому.
Каждая находка в секциях 1–2 несёт дополнительное поле:
```
- Действие: инлайн | развилка
```
`инлайн` — оркестратор чинит сам, не спрашивая и не логируя. `развилка` — цена
исправления сопоставима с переработкой, либо выбор меняет scope, либо решение
трогает инвариант: уезжает вопросом с вариантами и ценой каждого туда, где
проект держит вопросы, а работа продолжается на остатке.
Потребитель отчёта — оркестратор, который **реализует прочитанное**. Поэтому
потолок в 7 пунктов — не забота о внимании читателя, а защита кодовой базы от
правок, которых никто не заказывал.
@@ -0,0 +1,208 @@
# Бриф проекта — контракт
Конвейер общий, а находки — проектные. Проход, не знающий, что в этом проекте
нельзя нарушать, чем краснеет гейт и сколько данных реально проходит через узел,
выдаёт правдоподобные общие места: их дорого опровергать и нечем подтверждать.
Поэтому проектная специфика живёт **в одном файле проекта**, а не в charter'ах
агентов. Charter описывает **метод** прохода (что он делает и почему именно так),
бриф — **предмет** (что здесь дорого, чем это меряется, где лежит).
Шаблон для заполнения — [brief-template.md](brief-template.md).
## Где лежит и как находится
Порядок разрешения пути, одинаковый для скилла и для каждого агента:
1. путь, названный в задании конвейера (`бриф: <путь>`) — конвейер обязан его
передавать каждому проходу;
2. `docs/review-brief.md`;
3. `.claude/review-brief.md`;
4. брифа нет — **деградированный режим** (см. ниже).
Разрешает путь конвейер, один раз, и дальше передаёт готовым. Агент, получивший
путь в задании, сам ничего не ищет.
## Деградированный режим
Брифа нет — проходы работают, но их recall падает предсказуемым образом, и это
**обязано быть названо**, а не сглажено. Каждый проход без брифа:
- не присваивает `critical` по основанию «нарушен инвариант проекта» — инвариантов
он не знает;
- не оперирует числами объёма и потока — формулирует условиями;
- пишет в границы покрытия строку: «брифа проекта нет: инварианты, модель угроз и
профиль нагрузки неизвестны; находки этих классов не искались».
Триаж сводит эти строки в одну и выносит в финальный отчёт. Отсутствие брифа —
дыра покрытия, а не нейтральное умолчание.
## Форма
Markdown. Разделы — заголовки второго уровня с **точными именами** из списка
ниже: по ним агенты находят свой кусок. Порядок разделов свободен, лишние разделы
допустимы и игнорируются, отсутствующий раздел работает как деградированный режим
для тех проходов, которые его читают.
## Разделы
### `## Проект` — обязателен
Абзац: что система делает — и, что важнее, **чего она не делает**. Граница домена
нужна архитектурному проходу как критерий: «хранилище, а не аналитика», «единое
ядро, тонкие транспорты», «связующий сервис, а не медиатека». Без неё перенос
понятия через границу выглядит просто новым кодом.
Читают: `architecture`, `rubric`, `reimpl`, `specs`.
### `## Инварианты` — обязателен
Список того, что нарушать нельзя. Каждый пункт — три вещи:
- формулировка **как проверяемое свойство**, а не как лозунг: «точка сохраняется
дословно: незнакомое поле не отбрасывается», а не «бережно относимся к данным»;
- **последствие нарушения** и его обратимость;
- **severity по умолчанию** — если это не `critical`, скажи прямо.
Это единственный раздел, который **цитируется формулировкой**, а не пересказывается
ссылкой: по нему присваивается severity, и пересказ здесь стоит неверной оценки.
Читают: `specs` (режим 1 — отражены ли задетые инварианты в спеке), `code`,
`adversary`, `architecture`, `triage` (ранжирование и разметка «развилка»).
### `## Гейт` — обязателен
- **Команда** целиком, включая передачу базы диффа (`task gate BASE=<база>`), и
как база определяется по умолчанию.
- **Где логи** отдельных шагов.
- **Что означает каждый исход**: чем гейт краснеет, что предупреждает, что
пропускается по составу диффа.
- **Шаги, которые красят безусловно, и почему.** Это самая ценная часть раздела:
«данные под контролем версий», «структура конфига изменилась, а образец нет»,
«миграции не накатываются с нуля» — проход обязан знать, что здесь не бывает
«ну это мелочь».
- **Чего в гейте намеренно нет** и почему — прогон на живом корпусе, длинный
интеграционный тест. У проверки, которую гейт не гоняет, краснота никому не
видна; это уезжает в границы покрытия.
Читает: `gate`.
### `## Команды` — обязателен
Что проход имеет право выполнить и чем:
- **карта проекта для архитектуры** — команда, отдающая пакеты, граф зависимостей
и инвентарь концепций (`task review:context`);
- **запуск изменения вживую** — чем поднять и как проверить поведение (нужно
пайплайну задачи на шаге поведенческой верификации);
- **тесты, линт, дополнительные проверки** — и какие из них дорогие;
- **что запускать запрещено**: рабочая БД, боевой каталог данных, внешние
сервисы. Формулируй запретом с путями, а не «будь осторожен».
Читают: `architecture`, `gate`, `ops`, `triage`, пайплайн задачи.
### `## Прод и поток` — обязателен
Материал для эксплуатационного прохода, и он же — половина ранжирования триажа:
- где это работает: машина, окружение, что рядом, кто перезапускает;
- **внешние зависимости поимённо** и чем каждая отказывает: не только «падает», но
и «отвечает медленно», «молчит», «отдаёт мусор». Эксплуатационный проход
спрашивает про каждую отдельно, и список зависимостей он взять больше неоткуда;
- **кто заметит отказ и когда** — есть ли вообще наблюдатель;
- **характер потока**: непрерывный и молчаливый, по запросу, по расписанию; есть
ли обратная связь у отправителя;
- **измеренные числа с провенансом**: объёмы, размеры тел, темп, размеры таблиц.
Число без источника проход обязан превратить в условие — так и напиши, откуда
оно;
- **что обратимо, а что нет.** Падение, которое лечится повтором, и тихая потеря,
которую нечем восстановить, — разные классы, и порядок находок в отчёте зависит
от того, какой из них здесь главный.
Читают: `ops`, `adversary`, `triage`, `reimpl`.
### `## Модель угроз` — обязателен
- **что недоверенное** и каким каналом приходит: тело запроса, файл, аргумент
команды, ответ внешней системы, содержимое архива;
- **из чего строятся пути и ключи** — раскладка файлов на диске, состав
координатного ключа записи, имя каталога. Враждебный проход выводит запись за
пределы песочницы именно отсюда, и без этого пункта он ищет вслепую;
- **что разграничивает доступ** — токены, контуры, права файлов;
- **что чувствительнее чего**: если данные дороже секретов, скажи это прямо;
- **что вне модели** — перечислить явно. Пустой пункт «вне модели» означает, что
враждебный проход выдумает угрозу сам, и находка никогда не будет исправлена.
Читает: `adversary`.
### `## Карта` — обязателен
Где что лежит, путями:
- актуальные спеки и дельта-спеки предлагаемого изменения;
- конвенции прозой — и **какая их часть уже механизирована** правилом (её проход
по конвенциям не проверяет);
- архитектура и решения; журнал проскочивших дефектов;
- **единые точки проекта** — где генерируются идентификаторы и время, где
единственный парсер входного формата, где маппинг доменной ошибки в код ответа,
где общий путь приёма. Это материал для вопроса «не появился ли второй способ»;
если команда карты проекта их выгружает, здесь хватит ссылки на неё;
- **нумерованные артефакты** — путь миграций и правило нумерации: батч раздаёт
номера заранее, чтобы параллельные задачи не столкнулись файлами;
- где ведутся задачи (пайплайн только читает и сообщает исход);
- `testdata` и что в них лежит; куда можно писать временное;
- **каталоги, которые не трогают вовсе**.
Читают: все проходы.
### `## Типовые узлы` — необязателен, но без него рубрика беднеет
Роды узлов, из которых состоит проект (парсер входного формата, HTTP-обработчик,
репозиторий, воркер, клиент внешнего API, CLI-команда, файловое хранилище), и по
35 **специфичных для рода** проверяемых свойств к каждому.
Читает: `rubric`. Без раздела рубрика выродится в общие слова и повторит
конвенции — то есть станет applicative-проходом, ради отсутствия которого она и
существует.
### `## Триггеры` — необязателен
Проектная конкретизация правила выбора профиля: какие пути и контракты означают
`deep`; что считается «поведением, видимым снаружи»; при каком изменении
запускается `reimpl`. Умолчания записаны в самом скилле и работают без этого
раздела — но общее правило говорит «изменение публичного контракта», а какой
контракт публичный, знает только проект.
Читают: скилл конвейера, пайплайн задачи.
### `## Недоступно проверке` — обязателен
Что не проверит ни один проход и почему: поведение внешних систем и их будущих
версий, реальный профиль нагрузки, соответствие сохранённого действительности,
завязка внешних потребителей на текущую форму, суждение «а нужна ли эта
функциональность».
Этот раздел целиком уезжает в границы покрытия финального отчёта. Он существует
ровно затем, чтобы «критичных проблем не обнаружено» никогда не читалось как
«проверено всё».
Читает: `triage`; каждый проход — свою часть.
## Правила ведения
- **Бриф не пересказывает документацию проекта.** Факт, записанный в `CLAUDE.md`
или в архитектуре, попадает сюда ссылкой и одной строкой сути. Два дома для
одного факта разъезжаются, и разошедшийся бриф хуже отсутствующего: он выглядит
актуальным. Исключение одно — раздел инвариантов, он цитируется.
- **Числа — с провенансом.** «Тела доходили до 42 МБ (замер, ссылка)». Число без
источника проход не имеет права использовать как утверждение.
- **Что вне модели — называется явно.** Это относится и к угрозам, и к нагрузке,
и к классам находок, которые проект сознательно перестал проверять.
- **Бриф подчиняется промоуту.** Свойство, ставшее правилом линтера, из брифа
вычёркивается — как и из конвенций, и из charter'ов (см.
[promote.md](promote.md), шаг 3).
- **Когда обновлять:** сменился гейт; появился новый контур, зависимость или
источник входа; журнал ревью получил запись вида «проход не мог этого знать».
Планового пересмотра нет.
- **Бриф ведёт проект**, а не плагин. Плагин его только читает и никогда не
правит.
@@ -0,0 +1,93 @@
# Промоут: находка → конвенция → правило → удаление
Механизм храповика. Без него конвейер выдаёт одни и те же находки бесконечно, а
конвенции не растут — то есть внимание тратится повторно на уже решённое.
Роли уровней:
- **generative-проходы** — механизм *открытия* неявного (дорого, шумно, но
только они достают то, чего нет в списках);
- **конвенции** — дешёвая *регрессионная сетка* на уже открытое;
- **правила линтера** — то же с детерминированным оракулом и нулевой ценой
внимания.
## Шаг 1. Находка → конвенция
Условия: находка **принята** при ревью (не отвергнута, не понижена в гипотезу) и
**не специфична для одного места**.
- Формулируется как **проверяемое свойство**, а не как совет: «уровень доменного
отказа выбирает единственный логирующий чекпоинт», а не «внимательнее с
уровнями логов».
- Записывается источник — какой проход нашёл. Это единственные данные для
калибровки: проход, чьи находки регулярно доезжают до конвенции, оправдан;
проход, чьи находки не доезжают никогда, — кандидат на `drop` (см.
[calibration.md](calibration.md)).
- Место записи — файл конвенций проекта (путь — в разделе `## Карта` брифа). Если
тема относится к поведению системы, а не к тому, как мы пишем код, — это не
конвенция, а требование: заводится дельта-спека обычным путём.
Промоут идёт **тем же путём, что change → spec**: правка попадает в тот же
коммит, что и исправление кода, с пометкой в сообщении — история промоутов
остаётся видна в `git log` по файлу конвенций.
## Шаг 2. Конвенция → правило
Как только свойство выражается детерминированно, оно переезжает в инструмент.
Порядок предпочтения — от дешёвого к дорогому:
1. **готовое правило существующего линтера** — включить в конфиг;
2. **запрет идентификатора или импорта** правилом-«запретителем» с собственным
паттерном;
3. **правило с настройкой формы** — когда важно не имя, а конструкция;
4. **тест-сканер исходников** — когда правило про структуру проекта или про
схему: направление зависимостей, форма миграций, матчинг ошибки по тексту,
бизнес-логика в транспорте;
5. **собственный анализатор** — последний рубеж, заводим только если 1–4 не
выражают правило.
Правило обязано быть **зелёным на текущем коде в момент включения**: иначе
хук блокирует любой коммит, и правило снимут первым же раздражённым движением.
Приводить код в соответствие — часть шага 2, отдельным коммитом.
## Шаг 3. Удаление из конвенций, из брифа и из промптов
**Шаг, который пропускают чаще всего, и единственный, ради которого затевались
первые два.**
Как только правило работает:
- из файла конвенций убирается формулировка правила; остаётся, если нужно, одна
строка «проверяется линтером `<имя>`» — но только там, где без неё раздел
теряет связность;
- **из брифа проекта** убирается соответствующий пункт, а в разделе `## Карта`
правило переезжает в перечень «механизировано и потому проходом по конвенциям
не проверяется»;
- из контекста инструмента спек убирается дубль, если он там был.
Charter'ы проходов при этом **не правятся**: они общие и живут в плагине, а
предмет проверки приходит из брифа. Именно поэтому шаг 3 стал дешевле, чем был:
вычеркнуть строку в одном файле проекта, а не в девяти промптах.
Практический критерий: **в прозаических конвенциях остаётся только то, что
принципиально не выражается правилом.** Файл конвенций на несколько сотен строк
размазывает внимание модели по тривиальному — она добросовестно проверит
именование полей лога и не дойдёт до формы решения. Каждая строка конвенций,
которую можно было бы проверить машиной, оплачивается непойманным дефектом
где-то ещё.
## Обратное движение
Правило, которое даёт ложные срабатывания чаще, чем ловит (порядка трети от
общего числа), снимается и возвращается в прозу — или удаляется совсем, если
свойство перестало быть важным. Снятие фиксируется там же, где включалось, с
одной строкой «почему».
## Что промоуту не подлежит
- Находка, специфичная для одного места (её лечит комментарий в коде).
- Вкусовщина: не меняет поведения, не влияет на стоимость следующего изменения,
не нарушает записанного. Такое выбрасывается на триаже и не хранится.
- Свойство, требующее знания рантайма (профиль нагрузки, история инцидентов) —
его нельзя проверить ни промптом, ни линтером; место такому — в журнале ревью
как «признано неавтоматизируемым» (см. [review-journal.md](review-journal.md)).
@@ -0,0 +1,62 @@
# Журнал проскочивших дефектов
Артефакт проекта, а не плагина: файл живёт в репозитории (путь — в разделе
`## Карта` брифа, по умолчанию `docs/review-journal.md`). Здесь описано, зачем он
и какой формы, потому что без него конвейер не учится: находки закрываются,
причины непоймания теряются, и один и тот же класс проскакивает второй раз.
## Что туда попадает
Дефект, который **прошёл ревью и всплыл позже**. Записывается **сразу**, а не
ретроспективно: со временем теряется не сам факт, а причина непоймания —
единственное, ради чего журнал существует.
Реализованные задачи, находки ревью и принятые решения сюда не пишутся: у них
есть коммит, спека и задача. Здесь только промахи конвейера.
Отдельно сюда попадают **решения о составе прогонов**: перестали звать проход,
понизили профиль правилом, сузили класс проверяемого. Не потому, что это промах,
а потому, что здесь лежит цена: если что-то теперь проскочит, первый вопрос —
«не тот ли это класс, который мы перестали проверять».
## Форма записи
```
## ГГГГ-ММ-ДД — <краткое последствие>
- **Где:** путь:строка либо «конвейер, а не код»
- **Симптом:** как обнаружилось, кем и когда
- **Причина:** что на самом деле было не так
- **Почему не поймали:** какой проход обязан был найти и что ему помешало
- **Что меняем:** правило прохода, шаг гейта, конвенция, пункт брифа — либо
«ничего, цена поимки выше цены дефекта»
```
Последний пункт важнее остальных. Вывод «ничего не меняем» — законный исход: не
всякий дефект стоит того, чтобы усложнять ради него ревью каждой задачи.
## Куда ведёт запись
Три адреса, и выбор между ними — половина ценности журнала:
- **в бриф проекта** — если проход не мог знать факта: объём, характер потока,
что здесь необратимо, какой шаг гейта красит безусловно. Самый частый адрес и
самый дешёвый.
- **в конвенции или в правило линтера** — если свойство выражается
детерминированно (процедура — [promote.md](promote.md)).
- **в charter прохода** — если сломан **метод**, а не знание. Правка charter'а
меняет поведение во всех проектах, поэтому она требует калибровки
([calibration.md](calibration.md)) и обоснования, почему это не лечится
брифом.
## Что журнал даёт конвейеру
- **пробы для калибровки** — реальный проскочивший дефект сильнее синтетического:
синтетические смещены в сторону тех, которые уже умеешь придумывать;
- **основание для правил конвейера** — требование называть запущенные проходы
поимённо, отказ от чисел, производных от размера корпуса, и правило
последовательного прогона выведены из конкретных записей, а не из общих
соображений;
- **счётчик обратимости решений** — сузили состав проходов и через месяц поймали
дефект ровно того класса, который перестали проверять: решение пересматривается
фактом, а не спором.
+229
View File
@@ -0,0 +1,229 @@
---
name: task-batch
description: Проводит несколько задач разом — планирует порядок и пересечения, гонит каждую задачу отдельным сабагентом в своём git worktree через task-pipeline, интегрирует по одной ветке через rebase + fast-forward (линейная история), проверяет полноту ревью каждой ветки и в конце сверяет стыки, возникшие от слияния. Набор задач приходит извне. Использовать, когда просят сделать несколько задач сразу.
---
# Батч задач
Оркестратор **набора** задач. Планирует порядок, раскидывает задачи по
изолированным worktree, каждую проводит через полный цикл `task-pipeline`, затем
сводит в основную ветку линейной историей и делает финальную сверку. Тонкая
обёртка над `task-pipeline` — не переизобретай её шаги, вызывай как есть.
Работай **максимально автономно**, по тому же принципу, что и одиночный пайплайн:
вопрос, который решать не тебе, записывается и не останавливает поток; спрашиваем
только про **необратимое** (деплой, выкладка наружу, удаление или перезапись
рабочих данных). Механику — планирование, worktree, rebase, интеграцию, чистку —
делаем без спроса.
Перед стартом прочитай `CLAUDE.md` проекта и бриф ревью (`docs/review-brief.md`):
из него берутся команда гейта, инварианты и раскладка нумерованных артефактов.
## Границы
- **Набор задач приходит извне.** Батч его не формирует: не выбирает из беклога,
не приоритизирует, не решает, что важнее. Набор не задан — попроси его у
вызывающего и остановись.
- **Батч не владеет спринтом и целями.** Он сообщает исход по каждой задаче в тех
же трёх словах, что и `task-pipeline`: сделана / не доведена / оказалась крупнее
задачи.
## Ключевое отличие от одиночного пайплайна
`task-pipeline` коммитит **в текущую ветку**, и при ручном запуске это основная
ветка. Здесь так нельзя для параллельных задач, поэтому батч — **осознанное
исключение**: временные ветки и worktree заводятся лишь как средство изоляции, а
конечное состояние — та же линейная история основной ветки через rebase +
fast-forward. Ветки после вливания удаляются.
## Модель исполнения
- Каждая задача = **один автономный сабагент** (`general-purpose`, чтобы иметь
доступ к Skill и Agent для вложенных чекпоинтов ревью), работающий **только в
своём worktree** и прогоняющий `task-pipeline` целиком на этой задаче.
- Оркестратор кода задач не пишет: он планирует, заводит worktree, запускает
сабагентов, проверяет полноту их ревью, интегрирует ветки и делает финальную
сверку.
- Стиль правок внутри — заточка под проект и конвенции, right-size, без
золочения.
## Шаги
### 1. Прочитать набор
Набор задан списком (слаги, файлы, описания) — прочитай файл каждой задачи и
связанные спеки и черновики. Задачи-идеи включаются, но помни: сабагент проведёт
их сперва через `opsx:explore`, это тяжелее и чаще упирается в вопрос.
### 2. Спланировать порядок и пересечения (автономно)
Для каждой задачи определи:
- **затронутые capability** — по её описанию и по каталогу актуальных спек
(`openspec/specs/`);
- **жёсткие зависимости**: задача B строится на результате A → A строго раньше B;
- **нумерованные артефакты — номера раздаёт оркестратор заранее.** Если проект
нумерует миграции или подобные файлы (путь — из брифа), посмотри последний
номер и **раздай номера тем задачам, которые, вероятно, их добавят**, до
запуска. Номер уходит в charter сабагента, и он берёт назначенный, а не
«следующий свободный». Так такие задачи можно гнать одновременно: файлы не
столкнутся, а описание схемы правят разные строки — конфликт мелкий и решается
на интеграции;
- **жёстко сериализуем** (не гоняем одновременно) настоящие пересечения:
- **одна capability на несколько задач** — две задачи, правящие одну спеку (тем
более одно и то же `### Requirement`), дают не текстовый, а **семантический**
конфликт при архивации; сериализуем по смыслу, а не только по файлам;
- пересечение по одним и тем же исходникам;
- **мягкие конфликты** сериализовать не надо: индекс беклога (каждая задача
убирает свою строку) и спеки разных capability — разные строки и файлы,
сливаются сами.
Собери план: **волны** параллельно-безопасных задач плюс сериализованный хвост
конфликтоопасных, с учётом зависимостей. Покажи план короткой репликой и иди
дальше.
### 3. Свежая база
Убедись, что рабочее дерево чистое и основная ветка свежая. Зафиксируй базовый
коммит. Новые ветки бери от свежей вершины; ветки следующей волны — от вершины,
уже включающей результат предыдущих волн.
### 4. Прогнать волны
**Потолок параллелизма — 2–3 задачи одновременно.** Каждая задача тянет полный
`task-pipeline` с вложенным ревью и гейтом, поэтому больше трёх разом душат
машину и провоцируют гонки. Волну шире трёх бей на под-пачки по ≤3.
**Волна из одной задачи — не вырожденный случай, а обязательный.** Задача,
ревью которой будет доказывать находки **числами** (профиль `deep`, где работают
`adversary` и `ops`: удержание блокировки, пик памяти, рост файлов, длительность
операции), гонится в волне одна. Соседний прогон на той же машине портит эти
числа, а находка с испорченным оракулом хуже отсутствующей — она выглядит
доказанной. Если задача всё же пошла в общей волне, её отчёт обязан нести строку
в границах покрытия: замеры сняты под соседней нагрузкой.
Для каждой задачи в под-пачке:
1. Заведи worktree и ветку от текущей вершины:
`git worktree add <path> -b task/<slug> <основная ветка>`. Путь — во временном
каталоге проекта (`./tmp`), не в системном `/tmp`.
2. Запусти **по одному сабагенту на задачу, все в одном сообщении**,
`subagent_type: general-purpose`. Charter сабагента:
- работай **строго в своём worktree** `<path>`; в другие каталоги и в основную
ветку не лезь;
- прогони Skill **`task-pipeline`** ровно на этой задаче, полный цикл SDD с
обоими чекпоинтами ревью;
- если задаче назначен **номер артефакта** — используй строго его;
- **профиль ревью выбирается по факту изменения.** Батч не повод понижать
профиль: «нас много и мы спешим» — это ровно тот стимул, из-за которого
проходы пропускают;
- **режим прогона проходов — последовательный.** Твой worktree не один на
машине;
- **вернуть отчёт**, в котором обязательно: исход задачи одним из трёх слов;
что сделано; какие вопросы записаны и куда; изменённые файлы; добавлялся ли
нумерованный артефакт и с каким номером; затронутые capability; состояние
гейта; **перечень запущенных проходов ревью поимённо с исходом каждого** и
границы покрытия.
Сабагент, упершийся в вопрос, **не останавливает батч**: он записывает вопрос,
режет задачу до остатка и доводит остаток — либо, если остатка нет, возвращает
исход «не доведена». Оркестратор собирает такие вопросы и выносит их в финальный
доклад пачкой.
### 5. Проверить полноту ревью — до интеграции
**Ветка, чей отчёт не называет проходы поимённо, не вливается.** Пропуск прохода
не отличим от прохода без находок, и на уровне батча это ещё опаснее: отчётов
много, каждый выглядит полным, а сверять их некому, кроме тебя.
По каждой готовой ветке сверь перечень проходов с таблицей профилей скилла
`review-pipeline` для объявленного профиля. Расхождение — не повод отменять
задачу: дозапусти недостающие проходы **на ветке**, в её worktree, через
`review-pipeline`, и только потом интегрируй. Отчёт дозапуска приложи к отчёту
задачи.
### 6. Интегрировать — rebase + fast-forward, по одной ветке
Сводим ветки **строго последовательно** (линейная история), в порядке
зависимостей. Вливаем **только зелёные**.
Для каждой готовой ветки `task/<slug>`:
- `git rebase <основная> task/<slug>` — перенос на текущую вершину;
- резолв конфликтов (их почти нет — конфликтоопасное сериализовано, номера
розданы заранее). Неавтоматический конфликт — **не форсируй**: прерви
(`git rebase --abort`), оставь ветку и worktree как есть, вынеси это в доклад
как нераспознанное пересечение;
- `git checkout <основная> && git merge --ff-only task/<slug>`;
- после каждой интеграции — **гейт на основной ветке**. Красное — **откати эту
интеграцию** (`git reset --hard` на прошлую вершину), ветку с worktree сохрани,
задачу перечисли в докладе. Основная ветка **никогда** не остаётся
полузелёной;
- только после зелёного: `git worktree remove <path>` и
`git branch -d task/<slug>`.
**Политика частичного провала.** Упавшая задача (исход «не доведена», красные
тесты в её worktree, конфликт при rebase) **не блокирует остальные**: интегрируем
все зелёные, упавшую оставляем в её worktree и ветке нетронутой — ничего не
удаляем, — и перечисляем в докладе с причиной, отчётом и путём к worktree.
### 7. Финальный гейт
На основной ветке после всех интеграций — гейт целиком. Зелёное обязательно; пока
красное, шаг 8 не начинается.
### 8. Финальная сверка — только то, чего не видел никто
Каждая задача уже прошла полный конвейер в своём worktree. Повторять его на
интегрированном диффе бессмысленно: те же проходы на тех же файлах дадут те же
находки и удорожат триаж. Здесь проверяется **только то, что появилось от
слияния**:
- запусти **по одному `review-specs` на каждую затронутую capability**. Набор
назван поимённо и замеров эти проходы не делают, поэтому их допустимо гнать
одним сообщением — это то самое отступление от последовательного режима,
которое правило разрешает. Задание сузь до стыков: не сверять capability
целиком заново, а искать **рассинхрон код↔спека, возникший от слияния**
требование, которое одна задача выполнила, а соседняя незаметно отменила; два
change, по-разному описавшие одно поведение;
- если задачи пересекались по файлам, добавь один `review-architecture` на
интегрированный дифф с вопросом «не появился ли второй способ делать то, что
уже делается» — именно он возникает, когда две задачи независимо решали
похожее.
Замечания отрабатывай как в `task-pipeline`: `инлайн` чини сам, `развилка`
вопросом в запись; после правок — снова гейт.
### 9. Прибраться и доложить
- Убери worktree и ветки **только успешно влитых** задач, в конце
`git worktree prune`. Worktree и ветки **провалившихся** не трогай — они нужны
для ручного дожатия.
- Доложи кратко:
- **исход по каждой задаче** одним из трёх слов, с хешем коммита;
- план волн и порядок интеграции;
- вопросы, записанные сабагентами, пачкой;
- что дозапускалось на шаге 5 и почему;
- итог финальной сверки и ссылки на архивные change;
- **отдельно — провалившиеся** задачи с причиной и путём к оставленному
worktree;
- **границы покрытия сводной строкой**, включая задачи, чьи замеры снимались в
общей волне.
## Тонкости
- **Изоляция параллельных тестов.** Прежде чем гнать несколько прогонов разом,
убедись, что тесты не делят фиксированный порт или файл БД (обычно берут
временный каталог и эфемерный порт — тогда ок). Делят — гони такие задачи
последовательно.
- Поведенческая верификация внутри сабагента поднимает изменение вживую: следи,
чтобы соседние worktree не дрались за порты и рабочие каталоги. Если проект
умеет поднимать только один экземпляр — такие задачи в одну волну не ставь.
- Ревью выполненного — **до** закрытия задачи; это забота `task-pipeline` внутри
каждого сабагента, дублировать не надо.
- `openspec validate --strict` тоже внутри `task-pipeline` — не пропускай его
своими правками на интеграции.
- Крупная переработка, предложенная ревью внутри задачи, — развилка: не вливай
молча, вынеси в доклад.
- Держи вызывающего в цикле короткими репликами на переходах фаз (план → волны →
интеграция → финальная сверка), но не проси подтверждать механику.
@@ -0,0 +1,274 @@
---
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, не переизобретай их шаги.
Ревью — скилл `review-pipeline`, он же держит правило выбора профиля.
Перед стартом прочитай `CLAUDE.md` проекта и то, на что он ссылается
(архитектура, конвенции), если ещё не в контексте. Проектные факты, нужные ревью
— инварианты, команда гейта, объёмы, модель угроз, — живут в брифе
(`docs/review-brief.md`, контракт — в references конвейера ревью).
## Границы: чем пайплайн не владеет
- **Беклогом, спринтом, целями и приоритетами.** Задача приходит извне. Пайплайн
её не выбирает, не приоритизирует, не заводит и не переоценивает; если в
проекте есть свой процесс управления задачами — он и решает, что брать.
- **Определением ценности.** «Нужна ли эта функциональность» — не вопрос
пайплайна ни на одном шаге.
Пайплайн владеет **своим** определением готовности (ниже) и **сообщает
наблюдаемый исход**. Что с исходом делать дальше — не его дело.
## Наблюдаемые исходы
Ровно три, и каждый обязан быть назван в докладе прямо:
- **сделана** — определение готовности выполнено целиком;
- **не доведена** — с причиной и с записанным вопросом; что именно сделано и до
какой границы, названо явно;
- **оказалась крупнее задачи** — распознаётся **до заведения change**, иначе его
придётся выбрасывать. Дальше — декомпозиция, и это не работа пайплайна.
## Определение готовности
Задача сделана, когда верно всё:
1. гейт проекта зелёный;
2. ревью проведено **по профилю**, состав прогона сверен с таблицей профилей
поимённо, непущенные проходы названы в границах покрытия;
3. change заархивирован, дельты влиты в актуальные спеки;
4. коммит сделан в текущую ветку;
5. **критерии приёмки, если проект их дал**, проверены поимённо, у каждого назван
оракул и исход. Критерии приходят снаружи; пайплайн их не сочиняет и не
занижает. Расхождение «критерии закрыты, а суть задачи не достигнута» — дефект
критериев, и о нём сообщается, а не молча дорабатывается.
Пункты 1–4 — своё. Пункт 5 — внешнее, и проверяется только если оно дано.
## Принцип автономности
**Умолчание — делать, а не спрашивать.** Задача доводится до коммита без участия
человека; предполагается, что так пройдёт большинство задач.
Наткнулся на вопрос, который решать не тебе, — **не останавливайся и не
спрашивай**. Запиши его и продолжай:
1. **Запиши вопрос там, где проект держит вопросы** (секция беклога, файл
задачи, трекер — это знает проект). Если проект не сказал, куда, — отдельной
секцией `Вопросы` в своём докладе, и это тоже исход. Тело отвечает на три
вещи: **что именно решить**, **какие есть варианты и цена каждого**, **что
стоит, пока решения нет**. Плюс твоя рекомендация — человек чаще соглашается,
чем выбирает заново, и готовое суждение экономит ему весь контекст.
2. **Переформулируй задачу на остаток** — то, что делается без этого решения.
Назови границу: докуда доводим сейчас.
3. Доведи остаток до конца и закоммить. Задача не «висит на вопросе», она сделана
в объявленных границах.
**Что остатком не является** — две оговорки, без которых правило вредит:
- **остаток, который записывает в хранилище или в журнал состояние, зависящее от
нерешённого, — не остаток.** Решение поднимается до начала записи. Иначе
нерешённое материализуется в данные, а данные переживают решение;
- **остаток, из которого пропала польза, названная в постановке, — не остаток.**
Это исход «не доведена», а не «сделана в границах».
Нет полезного остатка — задача заканчивается исходом «не доведена», вопрос
записан, ничего не коммитится наполовину.
### Когда всё-таки спрашивать
Узко и по другому основанию — не «сложное решение», а **необратимое действие**:
- деплой, выкладка наружу, смена публичного адреса или токенов;
- удаление или перезапись рабочих данных, включая подрезку архивов;
- всё, что уходит за пределы машины.
Здесь ошибка не откатывается коммитом, поэтому спрашиваем даже когда решение
кажется очевидным. Развилка в дизайне — вопрос в запись; необратимое действие —
вопрос человеку сейчас.
Стиль правок — заточка под проект и конвенции, right-size, без золочения.
## Шаги
### 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 **`review-pipeline`** с профилем `design`, ссылкой
на change `<id>` и путём к брифу. Он запустит `review-specs` (режим «дизайн ДО
кода»), `review-rubric` (фаза 1: приёмочные критерии для задуманного узла) и
`review-architecture` по предложению.
Смысл профиля: архитектурная находка на готовом коде стоит переписывания и
потому игнорируется — та же находка здесь стоит абзаца обсуждения. Рубрику из
`review-rubric` перенеси в `tasks.md` как приёмочные критерии; там же уже лежат
критерии от постановки, если они были.
### 5. Отработать замечания ревью предложения
- Мелочь и явные улучшения — правь сам в спеках и дизайне.
- Развилки (компромисс, scope, инвариант) — вопросом в запись, спеки урезаются на
остаток.
- После правок перепрогони `openspec validate --strict <id>`.
### 6. Написать код — `opsx:apply`
Вызови Skill `opsx:apply` для реализации `tasks.md`. Код — по конвенциям проекта
(файл назван в разделе `## Карта` брифа). Меняешь схему — обнови её описание в
документации тем же change, если проект этого требует: гейт обычно это проверяет.
Прогони гейт и добейся зелёного — он же гейт следующего шага.
**Поведенческая верификация.** Если задача меняет реальное поведение (новый
эндпоинт, разбор входа, схема, форма ответа) — зелёных юнит-тестов мало. Подними
изменение вживую командой из раздела `## Команды` брифа и прогони сценарий.
Пропусти только для чисто внутренних правок без наблюдаемого рантайма.
**Сервис не оставляем лежать.** Если запуск упал — почини или откати до конца
шага.
### 7. Ревью кода — Skill `review-pipeline`
Второй чекпоинт. Вызови Skill **`review-pipeline`**, дав ссылку на change `<id>`,
базу диффа, путь к брифу, профиль **и режим запуска**. Профиль выбирается по
факту изменения, а не по ощущению важности; общее правило — в скилле, проектные
триггеры — в брифе:
- миграция схемы, новый пакет, публичный контракт, правило идентичности или
слияния данных → `deep`;
- иначе меняется поведение, видимое снаружи → `standard`;
- иначе (багфикс, локальная правка, доки) → `quick`.
**Режим по умолчанию последовательный, и обосновывать его не надо.** Параллельно
гоняем только тогда, когда об этом попросили явно **и назвали набор** — какие
именно проходы или какую стадию. Просьба без набора основанием не считается:
гони последовательно и скажи строкой, что набор не был назван. Причина умолчания
— замеры: `adversary` и `ops` доказывают находки числами, а два меряющих прохода
на одной машине портят числа друг другу; находка с испорченным оракулом хуже
отсутствующей, потому что выглядит доказанной.
Скилл сам гоняет гейт, нужные проходы и обязательный триаж. Возвращает отчёт с
потолком 7 пунктов, разметкой `Действие: инлайн | развилка` и секцией границ
покрытия.
**Сверь состав прогона с таблицей профилей в скилле, прежде чем коммитить.**
Пропуск прохода не отличим от прохода без находок: гейт зелёный, спеки сошлись,
отчёт выглядит полным. Единственный, кто мог бы заметить пропуск, — триаж, а он
заполняется тем, что ему подали. Отчёт обязан называть запущенные проходы
**поимённо и с исходом**; непущенный идёт строкой «не запускался» в границы
покрытия. Реестр короткий (4–8 проходов) — сверка стоит одного взгляда, а
молчащий пропуск уже стоил семи находок и отдельной задачи на их дозакрытие.
Отработай так же, как шаг 5: помеченное `инлайн` чини сам и не логируй,
`развилка` — вопросом в запись (он уже сформулирован триажем, его остаётся
перенести). После правок — снова гейт.
**Границы покрытия из отчёта не выбрасывай** — они уезжают в финальный доклад
сжатой строкой. Отчёт, из которого исчезло «что проверить было невозможно»,
превращается в ложное ощущение проверенности.
Отчёт триажа сохрани вместе с change (`openspec/changes/<id>/review/`): по нему
потом видно, что было найдено и что из этого осталось незаведённым.
### 8. Архивировать — `opsx:archive`
Вызови Skill `opsx:archive`: change уезжает в архив, дельты вливаются в
актуальные спеки.
### 9. Синк документации и закрытие задачи
Ревью выполненного — **до** закрытия. Затем:
- суть переехавшего решения — в документацию проекта (архитектура, журнал
решений), если её там ещё нет;
- менялась схема — её описание обновлено тем же change;
- новое, узнанное о внешнем формате или о данных, — в тот файл проекта, который
это накапливает; такой файл обычно ценнее кода;
- **задача закрывается процедурой проекта** — своей у пайплайна нет. Есть скилл
или скрипт беклога — вызови его; нет — скажи в докладе, что задача сделана и
закрытие остаётся за вызывающим. Не выдумывай формат чужого индекса.
### 10. Коммит
Коммить **в текущую ветку** (`git rev-parse --abbrev-ref HEAD`), сам ветку не
создавай и не переключай, ничего не пушь. При ручном запуске HEAD обычно на
основной ветке — коммит идёт прямо в неё; под оркестратором `task-batch` HEAD на
ветке задачи в изолированном worktree, и делать дополнительно ничего не нужно.
Сообщение — по-русски, скиллом `commit`, если он подключён (первая строка «что
сделано», тело списком 1–3 пункта, без трейлеров). Одна задача — один осмысленный
коммит.
Готово — доложи кратко:
- **исход** задачи одним из трёх слов и, если не «сделана», чем ограничен
результат;
- что сделано, какие вопросы записаны и куда;
- ссылка на архивный change;
- исход по каждому критерию приёмки, если они были;
- **одна строка границ покрытия**: какой профиль и режим гонялись, какие проходы
не запускались и что проверить было невозможно. Доклад без неё сообщает
«проверено», не сообщая, что именно.
## Тонкости
- **Не завязывайся на основную ветку и корень репозитория.** Скилл работает в
текущем worktree и на текущей ветке: не делай `git checkout`/`switch`, не
создавай веток, не пушь.
- Не пропускай `openspec validate --strict` перед архивацией.
- Тривиальная задача: шаги 2 и 4 пропускаются; ревью кода (шаг 7) остаётся
всегда, но в профиле `quick`.
- Гейт блокирует: пока он красный, опиниативные проходы не запускаются. Чинить и
перезапускать, а не «посмотреть заодно».
- Если ревью предлагает крупную переработку — это развилка: не правь молча и не
спрашивай, запиши вопросом и доведи остаток.
- Держи вызывающего в цикле короткими репликами на переходах фаз, но не проси
подтверждать механику.
- **Занизить профиль ревью или пропустить проход — самый дешёвый способ
«ускориться», и он же самый дорогой по последствиям.** Защита одна: профиль
выбирается по факту изменения, состав сверяется поимённо, а непущенное
называется в отчёте. Пропуск, названный строкой, стоит строки; пропуск молчащий
стоил семи находок.