ревью двумя проходами: 20 находок, все починены

Два независимых сабагента на av-dev-pm и av-dev-pipeline. Две находки нашли оба.

Главная — моя же перестановка закрытия за коммит сломала reopen и батч. close
печатал «дорога назад из git», а reopen искал коммит удаления, которого в новом
порядке ещё нет: шаг 11 последний, учёт остаётся незакоммиченным. Проверено
прогоном — отказ кодом 2 на свежезакрытой задаче. Тем же грязным деревом
ломались rebase и worktree remove в батче: каждая закрывшая задачу ветка уехала
бы в провалившиеся.

Починено с обеих сторон: reopen берёт текст из HEAD, если коммита удаления нет,
а шаг 11 коммитит учёт вторым коммитом.

Вторая — канонический пример docs/.pm.json убивал tasks.py. Четыре документа
показывали ключ tasks.sections, которого скрипт не знает: неизвестный ключ это
код 3 на любой команде. Проект, заведённый по канону дословно, остался бы без
работы с задачами, а docs.py при этом печатал «канон соблюдён». Секции живут в
заголовках индекса и второго дома не получают.

Остальные восемнадцать: init писал конфиг в упразднённый .tasks.json;
looks_like_tasks не видел переименованный индекс; урожай спринта терял автотег
после sprint close; ответ на вопрос по инструкции оставлял задачу незабираемой;
adopt требовал недостижимого зелёного; путь отчёта триажа не переживал archive;
review-specs не имел режима для стыка после слияния; три остатка «шаг 9а» несли
предкоммитную позицию закрытия; sprint.md отрицал сам себя в пункте «Сделана».

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
av
2026-08-03 15:54:15 +03:00
co-authored by Claude Opus 5
parent c692436b91
commit 1fb006df4a
18 changed files with 294 additions and 79 deletions
+28 -1
View File
@@ -1,6 +1,6 @@
---
name: review-specs
description: "Сверка изменения с дельта-спеками в обе стороны — spec→code (каждое требование реализовано и подтверждено тестом) и, что важнее, code→spec (поведение, которое код имеет, а спека не заказывала: тихие ветки, самодеятельные дефолты, проглоченные ошибки, отброшенные поля, ретраи «на всякий случай»). Плюс границы спеки — что она не определяет и что пришлось домыслить. Работает в двух режимах: дизайн/спеки ДО кода и код против спек ПОСЛЕ apply. Только чтение."
description: "Сверка изменения с дельта-спеками в обе стороны — spec→code (каждое требование реализовано и подтверждено тестом) и, что важнее, code→spec (поведение, которое код имеет, а спека не заказывала: тихие ветки, самодеятельные дефолты, проглоченные ошибки, отброшенные поля, ретраи «на всякий случай»). Плюс границы спеки — что она не определяет и что пришлось домыслить. Работает в трёх режимах: дизайн/спеки ДО кода, код против спек ПОСЛЕ apply и стык после слияния нескольких задач, когда change уже заархивированы. Только чтение."
tools: Read, Grep, Glob, Bash
model: opus
color: cyan
@@ -43,6 +43,12 @@ Development на OpenSpec). Оптика — требования, а не ст
намерение, а спека нормирует. Расхождение между proposal и дельтой — само по себе
находка.
**Исключение — режим 3 (ниже): живого change нет.** Тогда источник требований —
**актуальные** `openspec/specs/<capability>/spec.md`, а дельты поднимаются из
архива (`openspec/changes/archive/<id>/specs/`) как свидетельство о намерении
каждой слитой задачи. Задание обязано назвать этот режим явно; не названо —
работаешь по режиму 1 или 2 и говоришь в границах покрытия, что change не нашёл.
Дополнительно поднимаешь: `design.md` и `tasks.md` change, затронутые актуальные
спеки, инварианты из `CLAUDE.md`. Если тема ещё не перенесена в спеки и живёт
только в `docs/architecture.md` — источник истины там, и это фиксируется в
@@ -120,6 +126,27 @@ Development на OpenSpec). Оптика — требования, а не ст
скажи об этом прямо, с последствием. Такая находка всегда `Действие: развилка`:
менять спеку — решение человека.
## Режим 3 — стык после слияния нескольких задач
Зовётся финальной сверкой `task-batch`: несколько задач влиты в основную ветку,
их change **уже заархивированы**, живой дельта-спеки не существует. Предмет —
**только то, что появилось от слияния**, а не capability целиком заново: каждая
задача уже проверена в своём worktree, и повторение даст те же находки дороже.
Ищешь ровно три вещи:
- **отменённое требование** — одна задача его выполнила, соседняя незаметно
сняла; в актуальной спеке требование есть, в интегрированном коде его больше
нет;
- **два описания одного поведения** — два архивных change по-разному нормировали
одно и то же, и актуальная спека собрала из них противоречие;
- **осиротевшее поведение** — код, пришедший от слияния (разрешение конфликта,
правка при rebase), которого не заказывал ни один из change.
База — интегрированный дифф основной ветки против точки, с которой батч начался.
В границах покрытия скажи прямо: **capability целиком в этом режиме не
сверялась**, проверялись стыки.
## Чего этот проход принципиально не может поймать
- Качество формы решения: код может точно соответствовать спеке и быть плохим.
@@ -378,7 +378,7 @@ Recall обоих равен длине их источника — это и е
поэтому игнорируется; та же находка на предложении стоит абзаца обсуждения.
`rubric` живёт **только** в этом профиле. Судить код по критерию, под который он
писался, — корреляция по построению; те же 8–14 свойств уже лежат приёмочными
писался, — корреляция по построению; те же 8–12 свойств уже лежат приёмочными
критериями в `tasks.md`.
## Контракт находок
@@ -409,10 +409,15 @@ Recall обоих равен длине их источника — это и е
- Дефект, проскочивший ревью и всплывший позже, идёт в журнал проекта
([references/review-journal.md](references/review-journal.md)) — сразу, не
ретроспективно: теряется именно причина непоймания.
- **Отчёт триажа сохраняется вместе с изменением** (например, в
`openspec/changes/<id>/review/`). Он единственное, по чему потом видно, что
было найдено и что из этого не заведено: нулевой урожай при непустом отчёте
виден сразу.
- **Отчёт триажа сохраняется вместе с изменением** `openspec/changes/<id>/review/`.
Он единственное, по чему потом видно, что было найдено и что из этого не
заведено: нулевой урожай при непустом отчёте виден сразу.
**Вместе с изменением он и переезжает:** после `opsx:archive` его адрес —
`openspec/changes/archive/<id>/review/`. Кто ищет отчёт после архивации (батч
на финальной сверке, приёмщик на сессии), смотрит **оба** пути; «отчёта нет»
объявляется, только когда пуст и архивный, иначе самый дорогой сценарий
«состав ревью неизвестен, гоняем заново» срабатывает на каждой доведённой
задаче.
## Честный предел
@@ -26,7 +26,7 @@
| измеренные числа **с провенансом**, поведение внешних систем на самом деле | `docs/research/` |
| конвенции прозой и **что уже механизировано** правилом | `docs/conventions/` |
| почему решено так, отвергнутые варианты | `docs/adr/` |
| типовые узлы, типовые ложноположительные, вопросы к проходам, недоступно проверке | `docs/review.md`, раздел настройки |
| типовые узлы, типовые ложноположительные, вопросы к проходам, **триггеры профиля**, недоступно проверке | `docs/review.md`, раздел настройки |
| прецеденты: воспроизведённые дефекты с оракулом | `docs/review.md`, журнал |
| нормативное поведение и дельты изменения | `openspec/specs/`, `openspec/changes/<id>/specs/` |
+30 -13
View File
@@ -46,9 +46,11 @@ description: Проводит несколько задач разом — пл
- **Батч не владеет спринтом и целями.** Он сообщает исход по каждой задаче в тех
же трёх словах, что и `task-pipeline`: сделана / не доведена / оказалась крупнее
задачи.
- **Задачи закрывает пайплайн внутри каждого сабагента**, шагом 9а, вызовом
Skill `av-dev-pm:tasks`. Батч сам записей учёта не трогает: он не знает, чем
кончилась приёмка, и дублировать закрытие ему незачем. Урожай ревью батч
- **Задачи закрывает пайплайн внутри каждого сабагента**, шагом 11 — после
коммита работы и **отдельным коммитом учёта**, вызовом Skill `av-dev-pm:tasks`.
Батч сам записей учёта не трогает: он не знает, чем кончилась приёмка, и
дублировать закрытие ему незачем. Но грязное дерево после сабагента — **его**
проблема: на нём откажут и `rebase`, и `worktree remove` (см. шаг 6). Урожай ревью батч
отдаёт списком, а задачи из него заводит тот, кто ведёт задачи проекта.
## Ключевое отличие от одиночного пайплайна
@@ -183,7 +185,8 @@ fast-forward. Ветки после вливания удаляются.
записаны и куда; изменённые файлы; добавлялся ли нумерованный артефакт и с
каким номером; затронутые capability; состояние гейта; **перечень
запущенных проходов ревью поимённо с исходом каждого**; **путь к
сохранённому отчёту триажа** (`openspec/changes/<id>/review/`); шло ли ревью
сохранённому отчёту триажа** (`openspec/changes/<id>/review/`, после
архивации — `openspec/changes/archive/<id>/review/`); шло ли ревью
инлайн; границы покрытия.
Сабагент, упершийся в вопрос, **не останавливает батч**: он записывает вопрос,
@@ -204,7 +207,8 @@ fast-forward. Ветки после вливания удаляются.
не с чем; это само по себе основание не вливать, пока сабагент не назовёт
профиль и не обоснует его по факту изменения.
2. **Сверяй с независимым артефактом, а не с прозой отчёта.** Перечень проходов
бери из **сохранённого отчёта триажа** (`openspec/changes/<id>/review/`) —
бери из **сохранённого отчёта триажа** (`openspec/changes/<id>/review/` или
`openspec/changes/archive/<id>/review/` — задача доведена, change заархивирован) —
пайплайн обязан его туда положить. Проза сабагента написана тем же, кто мог
проход и пропустить: она подтверждает сама себя. Отчёта триажа на месте нет —
считай, что состав неизвестен, и дозапускай ревью целиком.
@@ -250,6 +254,11 @@ rebase делается **внутри worktree задачи**, а ff-слиян
розданы заранее). Неавтоматический конфликт — **не форсируй**: прерви
(`git -C <path> rebase --abort`), оставь ветку и worktree как есть, вынеси это
в доклад как нераспознанное пересечение;
- **дерево сабагента обязано быть чистым.** `git -C <path> status --porcelain`
до `rebase`: непусто — значит сабагент не довёл шаг 11 до коммита учёта (или
оставил мусор). Не форсируй и не коммить за него: назови задачу в докладе
недоведённой и оставь ветку с worktree. Молчаливый `rebase` на грязном дереве
всё равно откажет, но с сообщением про unstaged changes — а причина другая;
- **ненулевой код `rebase` относится к этой ветке и только к ней.** Прерванный
rebase в чужом worktree не трогает ни главное дерево, ни остальные ветки:
проверь `git -C <path> status` и `git status` — обе чистые. Уводить весь батч
@@ -286,17 +295,25 @@ rebase в файле X», а не «нераспознанное пересеч
находки и удорожат триаж. Здесь проверяется **только то, что появилось от
слияния**:
- запусти **по одному `review-specs` на каждую затронутую capability**. Набор
назван поимённо и замеров эти проходы не делают, поэтому их допустимо гнать
одним сообщением — это то самое отступление от последовательного режима,
которое правило разрешает. Задание сузь до стыков: не сверять capability
целиком заново, а искать **рассинхрон код↔спека, возникший от слияния**
- запусти **по одному `review-specs` на каждую затронутую capability**,
последовательно. Параллельно — только если человек попросил явно и назвал
набор: правило и оба его условия живут в
`av-dev-pipeline:review-pipeline`, раздел «Режим запуска», и здесь не
ослабляются. «Замеров не делают» основанием не является;
- **режим у этих проходов особый, и его надо назвать в задании.** Живого change
здесь нет — все заархивированы, дельта-спек не существует. Источник требований
**актуальные** `openspec/specs/<capability>/spec.md`, а предмет — стык:
требование, которое одна задача выполнила, а соседняя незаметно отменила; два
change, по-разному описавшие одно поведение;
архивных change, по-разному описавшие одно поведение. Скажи проходу это прямо,
иначе он пойдёт искать дельты и не найдёт ничего;
- если задачи пересекались по файлам, добавь один `review-architecture` на
интегрированный дифф с вопросом «не появился ли второй способ делать то, что
уже делается» — именно он возникает, когда две задачи независимо решали
похожее.
похожее;
- **заверши триажем.** Он единственный, кто агрегирует, и без него у находок нет
ни оракула, ни пометки `инлайн`/`развилка` — а следующий абзац на неё
опирается. Прогон из двух проходов без триажа — это сырые находки, выданные за
разобранные.
Замечания отрабатывай как одиночный пайплайн: `инлайн` чини сам, `развилка`
вопросом в запись; после правок — снова гейт.
@@ -307,7 +324,7 @@ rebase в файле X», а не «нераспознанное пересеч
`git worktree prune`. Worktree и ветки **провалившихся** не трогай — они нужны
для ручного дожатия.
- **Записей учёта батч не трогает** — их правит пайплайн внутри сабагента на
шаге 9а. Батч сообщает исход по каждой задаче; если какой-то сабагент дошёл до
шаге 11. Батч сообщает исход по каждой задаче; если какой-то сабагент дошёл до
коммита, но закрытия не сделал (плагина нет, вызов не разрешился), скажи это
строкой — иначе задача останется открытой молча.
- Доложи кратко:
+16 -4
View File
@@ -25,7 +25,9 @@ description: Автономно проводит одну задачу чере
`av-dev-pm:docs`, `av-dev-pm:tasks`. Короткое имя может разрешиться в
устаревшую проектную копию, и это произойдёт молча.
- **Проектные копии этих скиллов и агентов удаляются при установке плагина**
(`.claude/skills/{task-pipeline,review-pipeline,task-batch}`,
(`.claude/skills/` — и голые имена `task-pipeline`, `review-pipeline`,
`task-batch`, и с префиксом проекта: `<проект>-task-pipeline`,
`<проект>-review-pipeline`;
`.claude/agents/<проект>-review-*.md`). Две копии одного скилла расходятся, и
побеждает та, что короче названа.
@@ -45,7 +47,7 @@ description: Автономно проводит одну задачу чере
проекте есть свой процесс управления задачами — он и решает, что брать.
- **Форматом задач.** Пайплайн **не правит индексы руками и не выдумывает путь
к скрипту учёта**: он зовёт Skill `av-dev-pm:tasks`, который этим владеет
(шаг 9а). Закрытие как таковое — его работа, и это осознанное решение с
(шаг 11). Закрытие как таковое — его работа, и это осознанное решение с
названной ценой: **приёмщик и исполнитель совпали**. Закрытие поэтому **не
окончательно** — человек на сессии возвращает задачу `reopen` с причиной, а
доклад по критериям приёмки становится единственным, по чему приёмка вообще
@@ -259,7 +261,8 @@ description: Автономно проводит одну задачу чере
сжатой строкой. Отчёт, из которого исчезло «что проверить было невозможно»,
превращается в ложное ощущение проверенности.
**Отчёт триажа сохрани вместе с change (`openspec/changes/<id>/review/`) — это
**Отчёт триажа сохрани вместе с change (`openspec/changes/<id>/review/`; шаг 8
унесёт его в `openspec/changes/archive/<id>/review/` вместе с change) — это
обязательно, а не «если удобно».** По нему потом видно, что было найдено и что из
этого осталось в урожае. И это единственный **независимый** артефакт о составе
прогона: под оркестратором `task-batch` именно по нему сверяют полноту ревью
@@ -295,7 +298,7 @@ description: Автономно проводит одну задачу чере
основной ветке — коммит идёт прямо в неё; под оркестратором `task-batch` HEAD на
ветке задачи в изолированном worktree, и делать дополнительно ничего не нужно.
Сообщение — по-русски, скиллом `commit`, если он подключён (первая строка «что
Сообщение — по-русски, скиллом `av-dev-git:commit`, если он подключён (первая строка «что
сделано», тело списком 1–3 пункта, без трейлеров). Одна задача — один осмысленный
коммит.
@@ -309,6 +312,15 @@ description: Автономно проводит одну задачу чере
**Порядок обязателен.** Закрытие удаляет файл задачи; сделанное до коммита оно
оставило бы задачу закрытой без единого следа работы, если шаг 10 упадёт.
**Закрытие тоже коммитится — вторым коммитом, тут же.** Удаление
`items/<slug>.md` и правка `SPRINT.md` — это правки в рабочем дереве, и оставить
их незакоммиченными нельзя по трём причинам: `task-batch` следом делает `rebase`
и `worktree remove`, а те откажут на грязном дереве; закрытие, не доехавшее до
основной ветки, оставит задачу открытой молча; и опора «`SPRINT.md` под git
показывает, что и когда закрыто» без коммита — пустые слова. Сообщение короткое,
про учёт, а не про работу: `закрыта задача <slug>`. Это второй коммит осознанно:
правило «одна задача — один осмысленный коммит» про работу, а учёт — не работа.
Плагина в проекте нет — вызов не разрешится. Тогда **ничего не выдумывай**:
скажи в докладе, что учёт задач остаётся за владельцем, и назови исход.