Тема
Кросс-модельное ревью замысла — SSOT
Ревью кода ловит расхождение кода с замыслом. Против ошибки в самом замысле оно бессильно: неверно записанное требование закрепляется зелёным тестом и вскрывается только в бою. Прецедент — exit-shortfall-deadlock (2026-08-21): временное отставание учёта биржи было записано в требование как свойство позиции, тест a_shortfall_below_the_min_lot_stops_attempting был ЗЕЛЁНЫМ, две позиции остались без выхода и без стопа на 14 минут. Поэтому ревью стоит на артефактах замысла, а не только на диффе.
Решение оператора 2026-08-26: ревью обязательно на этапе дизайна и при изменении документации и openspec, и оно всегда кросс-модельное — работу Claude Code ревьюит Codex, работу Codex ревьюит Claude Code.
Два круга
Схема OpenSpec spec-driven-crossreview (openspec/schemas/spec-driven-crossreview/) добавляет к штатным артефактам два:
| Артефакт | Файл | Требует | Что проверяет |
|---|---|---|---|
review-requirements | review/requirements.md | proposal, specs | замысел и формулировки требований |
review-design | review/design.md | design, tasks | решение и разбивку работ |
Граф зависимостей меняется так, что круг нельзя ПРОПУСТИТЬ — но проверяет он только НАЛИЧИЕ квитанции, а не её вердикт (см. «Чего гейт НЕ доказывает»):
designтребуетreview-requirements— файл квитанции круга 1 обязан существовать;apply.requires: [tasks, review-design]— то же для круга 2 перед реализацией.
Разрешающий ли в квитанции вердикт, граф не смотрит: это работа гейта.
openspec status --change <name> показывает это состоянием blocked by, а openspec instructions apply отвечает ⚠️ Blocked. Missing artifacts: ….
Схема — умолчание проекта (openspec/config.yaml), поэтому новые карточки получают круги автоматически.
Кто ревьюер
| Артефакты писал | Ревьюер |
|---|---|
| Claude Code | Codex CLI — scripts/codex-review.sh (поверх codex exec, read-only, без approvals) для диапазона коммитов против документа-оракула; codex review --commit <sha> для одного коммита; codex review --base main для диффа ветки |
| Codex | Claude Code |
Ревью в том контексте, который писал артефакты, кругом не является. Если кросс-модельный вызов не прошёл (не установлен, ошибка, пустой ответ) — вердикт не выдумывается и на саморевью не подменяется: работа останавливается, отказ выносится оператору.
Обратное направление (Claude Code ревьюит Codex) форматом квитанции поддержано — поля Author/Reviewer направления не фиксируют, — но в этой установке не прогонялось: проверены только квитанции с Reviewer: codex.
MCP-сервер Codex снят решением оператора 2026-09-15. Замер того же дня: codex-cli 0.154.0, codex help mcp-server отвечает error: unrecognized subcommand 'mcp-server'; без help слово уходит в интерактивный режим как промпт и падает с Error: stdin is not a terminal, что Claude Code показывает как CONNECTION_CLOSED. Запись codex в ~/.claude.json и вызовы mcp__codex__codex / mcp-cli call codex/codex в определениях агентов tech-lead и codex-debater (~/.claude/agents/) рабочим путём не являются; ревью ведётся только командами CLI из таблицы выше.
Модель ревьюера
- Модель
gpt-5.6-luna,model_reasoning_effort = medium, без запасной (решение оператора 2026-09-15, вечер). Основание — живой ответ API при попытке круга requirements наgpt-5.3-codex-spark: HTTP 400invalid_request_error, «The 'gpt-5.3-codex-spark' model is not supported when using Codex with a ChatGPT account».~/.codex/config.tomlуже держитmodel = "gpt-5.6-luna"по умолчанию. Запасной модели при этом решении нет: при исчерпании лимита использования или любом другом отказе кросс-вызова работа останавливается до решения оператора, а не подменяется саморевью или другой моделью. - История решений: 2026-09-01 —
gpt-5.6-sol,high(снято); 2026-09-15 утром — основнаяgpt-5.3-codex-spark,medium, запаснаяgpt-5.6-lunaпри исчерпанном лимите использования (наблюдённый текст 2026-09-01: «You've hit your usage limit … try again at …»; снято тем же днём вечером после отказа API выше); 2026-09-15 вечером — текущее решение. - Исполнение:
scripts/codex-review.shвызываетgpt-5.6-lunaнапрямую — ветки переключения на запасную модель в скрипте больше нет. Прямойcodex reviewна модель по умолчанию из~/.codex/config.tomlне полагается: модель и reasoning передаются явно —-c model="gpt-5.6-luna" -c model_reasoning_effort="medium". - Прямой
codex review(замер 2026-09-15,codex-cli 0.154.0): песочница по умолчаниюworkspace-write [workdir, /tmp, $TMPDIR], и ревьюер сам запускает команды — в замере он собралpm-commonчерезcargo testв/tmp/cargo-target-pm-common(714 МБ) без слота и без потолка параллелизма. Поэтому обязателен-c sandbox_mode="read-only"(в шапке прогона должно стоятьsandbox: read-only).--commit/--baseне совместимы с собственным промптом (the argument '--commit <SHA>' cannot be used with '[PROMPT]'): бриф по категориям правила 12 и документ-оракул передаются только черезscripts/codex-review.sh;codex reviewидёт со встроенным промптом Codex и отвечает по-английски. Из агентной песочницы обе команды не запускаются (~/.codexнедоступен на запись) — вызов делается вне песочницы, каждый вызов считается кругом по правилу 9. - Именно эта модель, а не другие:
gpt-5.2,gpt-5.2-codex,gpt-5.3-codex-sparkаккаунт ChatGPT отвергает (замеры 2026-09-01 и 2026-09-15: «The '…' model is not supported when using Codex with a ChatGPT account»). Замер 2026-09-15 отдельно для прямогоcodex review:codex review --commit … -c model="gpt-5.3-codex-spark"вердикт вернул, тогда какcodex exec -m gpt-5.3-codex-sparkтем же днём тот же аккаунт отверг — разные команды по-разному проверяют доступность модели. Правило от этого не меняется:gpt-5.6-lunaвезде, и дляcodex exec, и дляcodex review, — расхождение поведения команд не основание переносить исключение на другой путь ревью.
Квитанция
Шапку читает машина, поэтому формат обязателен:
Author: claude-code
Reviewer: codex
Round: requirements
Round-No: 1
Reviewed-Paths: docs/target/engine.md, openspec/specs/directional-stop-loss/spec.md
Reviewed-Digest: <вывод design-review-gate.sh digest>
VERDICT: APPROVE
CHANGES_APPLIED: n/aVERDICT— ровно одно изAPPROVE,APPROVE_WITH_CHANGES,REVISE.APPROVE_WITH_CHANGESоткрывает дорогу дальше только вместе сCHANGES_APPLIED: yes, который автор ставит после того, как каждая названная правка внесена и ревьюер перепроверил именно их.- Открытая находка
[CRITICAL]приVERDICT: APPROVE— недействительная квитанция. - Раздел «Разбор замечаний» обязателен, и в нём ОДНА строка на находку строгого вида:
- <находка> → исправлено: <что именно>либо- <находка> → отклонено: <причина>. Строк должно быть не меньше, чем находок. Причина обязана быть непустой — «отклонено:» без текста строкой разбора не считается. Строки внутри ограждённого блока кода (```) — пример формата, а не разбор, и не засчитываются. - После двух подряд вердиктов
REVISEработа останавливается и выносится оператору. - Очередной раунд назначается по целесообразности, а не по привычке: он стоит времени оператора и расхода модели. Признак исчерпанной отдачи — раунд приносит всё более узкие находки об одном и том же месте, и каждая правка порождает следующую такую же. Тогда найденное исправляется, круг закрывается, а остаточный риск называется в квитанции. Раунды сверх этого — злоупотребление ресурсами оператора, а не тщательность (решение оператора 2026-08-28 по итогам 3 и 7 раундов этой карточки; механической проверки числа раундов намеренно НЕТ — гейт
Round-Noне читает).
Слепок: чем ловится устаревание
Вердикт относится к точному содержимому проверенных файлов. Слепок получают командой:
bash
scripts/design-review-gate.sh digest --change <name> --round requirements \
[--path docs/target/engine.md]...Слепок берётся от списка пар «путь + хеш», а не от файлов по отдельности: иначе spec-файл, добавленный уже после вердикта, остался бы непроверенным и слепок бы не изменился.
tasks.md входит в слепок нормализованным — отметки - [x] приводятся к - [ ]. Без этого первая же закрытая задача обесценивала бы вердикт круга 2 и требовала нового ревью на каждую отметку прогресса: правило такой цены обходят, а не исполняют. Нормализация ровно та же, что в определении «касания карточки», поэтому два правила не могут разойтись. Изменение текста задачи вердикт по-прежнему обесценивает.
Любая другая правка проверенных файлов, кроме внесения названных ревьюером исправлений, делает вердикт недействительным — круг проходится заново.
Гейт
Проверка стоит в двух местах:
scripts/gate.sh— блокирующим шагом до компиляции (run_blocking_step): отсутствующая или устаревшая квитанция отказывает за секунду, а не после восьми минут сборки, и дальнейшие шаги не запускаются;pre-commit(ставитсяscripts/install-git-hooks.sh) — доcargo fmtи до выхода по отсутствиюcargo: правки документации и openspec бывают в дереве, где Rust-инструментов нет вовсе.
Проверка в pre-commit активна только после переустановки хука. Каталог .git/hooks не версионируется, поэтому обновлённый скрипт сам по себе ничего не меняет: пока оператор не выполнит scripts/install-git-hooks.sh в конкретном рабочем дереве, там продолжает работать прежний хук, проверяющий одно форматирование. Отличить версии можно по маркеру в .git/hooks/pre-commit: …:pre-commit:v1 — без проверки ревью, …:pre-commit:v2 — с проверкой ревью и с cargo fmt --all -- --check по всему дереву, …:pre-commit:v3 (решение оператора 2026-09-09) — та же проверка ревью, но форматирование сужено до застейдженных .rs файлов (rustfmt --check по списку из git diff --cached) — при параллельных агентах в одном worktree чужой неотформатированный файл, не идущий в коммит, блокировал его в v2. …:pre-commit:v4 (тот же день) сузил так же саму проверку ревью замысла: design-review-gate.sh check сам зовёт git diff <база> по рабочему дереву, а не по индексу — в v3 чужая незакоммиченная правка tasks.md/AGENTS.md в общем worktree делала квитанцию «устаревшей» и блокировала коммит остальных агентов, даже по непересекающимся файлам. v4 экспортирует индекс (git checkout-index -a) во временный каталог и гонит проверку там (GIT_DIR — на настоящий репозиторий, GIT_WORK_TREE и GIT_INDEX_FILE — на временный каталог и его собственный index): диффится база против того, что реально попадёт в коммит, а не против чужих правок рядом. Обновление прежней нашей версии --force не требует. Единственный рубеж, работающий в любом дереве без установки, — блокирующий шаг ворот.
База сравнения: для ветки, отличной от main, — git merge-base main HEAD; на main — HEAD, то есть проверяется незафиксированное (полный прогон ворот делается до коммита). Уже сделанный коммит проверяется явной базой: check --base HEAD~1.
Вслепую проверка не работает и молча не деградирует: база, которая не разрешается в коммит, и случай «ветка не main, а локальной ссылки main нет» останавливают её с кодом 70. Молчаливый откат к HEAD во втором случае означал бы, что закоммиченная работа ветки исчезает из проверки, а гейт объявляет ложный успех.
Что гейт проверяет
Затронутая карточка стоит на схеме с ревью; иначе — отказ с указанием перевести её.
Квитанция круга существует;
AuthorиReviewerзаполнены, различаются и принадлежат известному множеству сторон (claude-code,codex) — произвольная строка означала бы, что различие ничего не значит.Вердикт разрешающий,
APPROVE_WITH_CHANGESподтверждён,REVISEотказывает.Вердикт не противоречит собственным находкам. Проверяется механически в двух точках: открытая находка
[CRITICAL]запрещаетAPPROVE, и число находок не должно превышать число строк разбора. Строка разбора имеет ЖЁСТКИЙ формат — начинается с «- », содержит « → » и слово «исправлено» либо «отклонено», — и считается только внутри раздела «Разбор замечаний». Формат выбран вместо markdown-таблицы после четырёх кругов ревью, каждый из которых находил новую границу грамматики таблиц (разделитель без шапки, ведущие пробелы, отступ в четыре пробела как код-блок): разбор таблиц регулярными выражениями порождает границы быстрее, чем их закрывают. Закрытость находки ПО СУЩЕСТВУ машиной не проверяется.Слепок вычисляется и совпадает с текущим содержимым. Невычислимый слепок (например,
Reviewed-Pathsназывает несуществующий файл) — отказ, а не пропуск.Каждый затронутый файл
openspec/specs/**— любого типа, не только*.md— назван вReviewed-Pathsпринятой квитанции. Обычная документация (docs/**,CLAUDE.md,README.md,CONTRIBUTING.md) с решения оператора 2026-08-30 квитанции не требует: гейт вместо неё требует строку вCHANGELOG.mdв том же диффе (CHANGELOG.mdсам по себе документацией не считается и правило на него не действует). Рабочая память модулей —AGENTS.mdна любом уровне и вложенныеCLAUDE.md-симлинки — с решения оператора 2026-09-02 к обычной документации НЕ относится: это тот же класс проверки, чтоopenspec/specs/**— путь либо назван вReviewed-Pathsпринятой квитанции, либо снят явным обходом с причиной; строкаCHANGELOG.mdэту проверку не заменяет. Основание: поAGENTS.mdагент правит модуль, расхождение памяти с кодом — дефект той же тяжести, что сломанный тест. КорневойCLAUDE.mdостаётся обычной документацией.На ветке
main(решение оператора 2026-09-15) правило «CHANGELOG.mdв том же диффе» СНЯТО и заменено отложенным долгом ровно на один коммит. Требовать хеш коммита в файле, который сам этот коммит и коммитит, — зацикленность: до самого коммита его хеша не существует, и автор физически не может вписать его заранее. Повод — записиCHANGELOG.mdот 2026-09-15, называвшие хеш родителя docs-коммита вместо самого коммита (пример: строка про модель ревьюераgpt-5.3-codex-spark, добавленная коммитом8693cc4b, называла207f52dd— совсем другой коммит роадмапа); той же правкой найдены и исправлены ещё три такие записи в файле. Механика (scripts/design-review-gate.sh,check_changelog_ledger_debt): docs-коммит на main проходит гейт без строкиCHANGELOG.mdвовсе, а СЛЕДУЮЩИЙ коммит на main отказывает, пока вCHANGELOG.mdне появится строка, чей хеш (последнее поле, обрезанное по пробелам) — префикс полного sha ЭТОГО коммита, а не его родителя. Опора диапазона проверки — самый старый first-parent коммит main, где в самом скрипте впервые появился маркерCHANGELOG_LEDGER_LAG_MARKER, — вычисляется динамически при каждом запуске, а не прошита sha, и потому не требует ручной правки при взрослении истории репозитория. Если маркера в first-parent истории нет, гейт различает два состояния по ЗАКОММИЧЕННОЙ (HEAD) версииscripts/design-review-gate.sh— под крюкомGIT_DIRуказывает на настоящий репозиторий, иgit show HEAD:…возвращает ту же историю, что видитgit log, даже из экспортированного во временный каталог индекса:- в
HEAD-версии маркера тоже нет — бутстрап. Коммит, который его вводит, крюк pre-commit проверяет ДО того, как этот коммит попадёт вgit log(маркер в этот момент виден только в застейдженном файле). Правило считается ещё не введённым, и долг не проверяется: иначе коммит, вводящий правило, требовал бы строкCHANGELOG.mdза всю историю репозитория (сотни коммитов), которые ни для одного из них никогда не требовались, и тем самым сам ввод правила стал бы невозможен — ровно так и произошло при слиянииfix/changelog-ledger- lagвmain2026-09-15: крюк pre-commit запустил стейдженную версию скрипта, опора не нашлась, и прежняя редакция откатывалась на корневой коммит репозитория. - в
HEAD-версии маркер ЕСТЬ, аgit log --first-parent -Sего не находит — опора ПОТЕРЯНА, гейт отказывает (fail-closed), а не пропускает. Правило уже действует (маркер закоммичен), но диапазон долга вычислить нечем — это не бутстрап, а признак того, что first-parent цепочка отHEADсломана или неполна (например, локальное повреждение репозитория — недостающий commit-объект: живым прогоном проверено, чтоgit log -Sв этом случае отказывает ненулевым кодом и печатает пустойstdout, хотяgit show HEAD:<путь>по тому же файлу отрабатывает штатно, поскольку блоб текущего дерева адресуется по содержимому независимо от повреждения commit-графа). Переименование литерала в это состояние НЕ попадает:-Sнаходит коммит переименования как первое появление нового имени — опора просто сдвигается вперёд, это исход «найдена», а не «потеряна» (проверено живым прогоном). Молчаливо трактовать потерянную опору как бутстрап означало бы пропустить накопленный долг вслепую именно тогда, когда история уже настоящая, а не ещё не начатая. У литерала-маркера вscripts/design-review-gate.shРОВНО ОДНО вхождение — присваивается локальной переменнойmarkerвнутриchangelog_ledger_anchor, а сравнение (-S,grep -c) идёт через эту переменную (grep -c <литерал> scripts/design-review-gate.shобязан давать 1). Отсюда два жёстких требования к правкам этого места: литерал нельзя переименовывать (новое имя как «первое появление» сдвинет опору на коммит правки, и весь накопленный долг перестанет проверяться) и нельзя заводить его вторые вхождения, включая пояснительные комментарии (git log -Sищет коммит, где ОБЩЕЕ ЧИСЛО вхождений строки изменилось — лишнее вхождение либо создаст более раннюю ложную опору, либо не даст найти искомый коммит вовсе). Для merge-коммита в зачёт идёт дифф СТРОГО против его первого родителя (что реально изменилось на main в результате слияния); отдельные коммиты влитой ветки first-parent-обход не видит и не требует называть по отдельности. На ветках, отличных отmain(база сравнения —merge-baseсmain), правило «CHANGELOG.mdв том же диффе» действует без изменений.
- в
Круг 2 требуется, как только появился любой из
design.md/tasks.md: требование обоих означало бы, что удаление одного файла отключает проверку.Изменение самой квитанции карточку из проверки не выводит — иначе подмена или удаление единственной квитанции прошли бы незамеченными.
Чего гейт НЕ доказывает
Он не доказывает, что ревью действительно провела вторая модель. Поля Author и Reviewer заполняет тот же процесс, который писал артефакты, а независимого следа вызова, проверяемого машиной, нет: ~/.codex/session_index.jsonl хранит только {id, thread_name, updated_at}, а файлы rollout-*.jsonl в ~/.codex/sessions/ в этой установке обрываются мартом 2026 (проверено 2026-08-26). Кросс-модельность держится на дисциплине и на инструкции артефакта.
Блокировка графа OpenSpec — по наличию файла, а не по содержанию вердикта. Квитанция, оставленная в виде шаблона со значением VERDICT: REVISE, в глазах CLI разблокирует design и apply. Содержание вердикта проверяет только design-review-gate.sh — из ворот и из pre-commit. Официальная документация OpenSpec говорит о своём графе то же самое: «OpenSpec only checks that artifacts exist, so enforce the gate with your own CI or hook» (docs/customization.md, раздел Community Schemas).
Оба ограничения названы, а не подразумеваются: первое закрывается только следом вызова, которого в системе нет, второе — только проверкой вне CLI, и она сделана.
Правки вне карточки
Правка openspec/specs/** или рабочей памяти модулей (AGENTS.md, вложенные CLAUDE.md) обязана быть покрыта квитанцией: путь называется в Reviewed-Paths и входит в слепок. Обычная документация docs/** квитанцией не проверяется — гейт требует строку CHANGELOG.md (см. выше); поведенческая правка документации ведётся через карточку дисциплиной процесса, машиной этот выбор не проверяется. Отдельного хранилища квитанций для правок вне карточки намеренно нет — второй носитель того же знания стал бы вторым ответом на один вопрос.
Правка, поведения не меняющая (опечатка, ссылка, переформатирование), снимается осознанным обходом — он печатается в отчёте, а не действует молча:
bash
DESIGN_REVIEW_EXEMPT="crates/pm-engine/AGENTS.md:правка ссылки, поведение не меняется" scripts/gate.shРазделитель записей — точка с запятой: причина сама содержит запятые. Причина ОБЯЗАТЕЛЬНА: запись без двоеточия или с пустой причиной отбрасывается с сообщением и требование ревью не снимает — обход без названного обоснования это не осознанное решение, а тихая дыра.
Существующие карточки
На момент введения правила активных карточек 70, все на схеме spec-driven. Умолчание сменилось только для новых. Действующие переводятся при следующем касании: openspec/changes/<name>/.openspec.yaml → schema: spec-driven-crossreview, затем проходятся оба круга.
Отметка выполнения задачи (- [ ] → - [x]) касанием замысла не считается: гейт сравнивает нормализованные версии tasks.md и пропускает правку, сводящуюся к переключению чекбоксов. Изменение текста задачи ревью требует.
Проверенные образцы поведения
Образцы оформлены воспроизводимым набором scripts/test-design-review-gate.sh (42 случая, каждый в отдельном временном git-репозитории). Набор вызывается блокирующим шагом ворот перед самим гейтом: сломанный гейт печатает «пройдено» молча, поэтому обнаружиться он обязан раньше, чем что-то подтвердит. Разовые прогоны для этого не годятся — они не ловят регрессию.
Замеры 2026-08-26, дополнены 2026-09-02 (память модулей и пересечения классов). Каждый сценарий дал ожидаемый код возврата.
Отказывают: карточка на старой схеме; отсутствующая квитанция; удаление квитанции как единственное изменение; REVISE; APPROVE при [CRITICAL]; совпадение Author и Reviewer; сторона вне множества; APPROVE_WITH_CHANGES без CHANGES_APPLIED: yes; устаревший слепок после правки проверенного файла; правка текста задачи; маркер - [x] внутри текста задачи; Reviewed-Paths с несуществующим файлом; правка openspec/specs/** без покрытия; правка AGENTS.md или вложенного CLAUDE.md без покрытия — на любом уровне, включая docs/ и каталог карточки, и строка changelog её не заменяет; наличие tasks.md без квитанции круга 2; находок больше, чем строк разбора (в том числе на одну строку); строки разбора не в том формате, с отступом, ниже следующего заголовка, внутри блока кода, с «отклонено» без причины и вырожденные (пустое имя находки, второй маркер вместо причины); обход без причины и с пустой причиной; база, не разрешающаяся в коммит, и ветка без ссылки main (код 70).
Проходят: корректная квитанция; строк разбора ровно столько же, сколько находок; путь, названный в Reviewed-Paths, — включая имя с пробелами и расширением, отличным от .md; обход с причиной, содержащей запятую, — в том числе на файле внутри целиком неотслеженного каталога (сборщик изменений перечисляет их поимённо, -uall); переключение чекбоксов в tasks.md; AGENTS.md, названный в Reviewed-Paths квитанции; корневой CLAUDE.md со строкой changelog; удалённая целиком карточка (одна пояснительная строка, без отказа).
Два дефекта найдены именно этими прогонами и исправлены: command substitution выбрасывает NUL-байты, из-за чего -z-вывод git склеивал пути в один; read без завершающего перевода строки терял единственный элемент Reviewed-Paths, и путь молча выпадал из слепка.
Известные компромиссы
- Форк замораживает инструкции штатных артефактов на версии OpenSpec 1.10.0: обновления пакета в
openspec/schemas/spec-driven-crossreview/сами не прилетят. Расхождение с пакетной схемой отслеживается сравнениемopenspec/schemas/spec-driven-crossreview/schema.yamlс$(npm root -g)/@fission-ai/openspec/schemas/spec-driven/schema.yamlпри обновлении CLI. - В CI шага нет.
.github/workflows/parity.ymlиспользует мелкий checkout, при которомmerge-baseсmainнедоступен и проверка прошла бы вхолостую, давая ложное «пройдено». Это третий шаг ворот, которого в CI нет, — наряду сcargo fmt --all -- --checkиopenspec validate --all --strict. CI и без того не запускается: у репозитория нет удалённого.
Источники
- Схема и инструкции кругов:
openspec/schemas/spec-driven-crossreview/schema.yaml - Гейт:
scripts/design-review-gate.sh, вызов —scripts/gate.sh - Официальная документация OpenSpec: https://github.com/Fission-AI/OpenSpec/raw/refs/heads/main/docs/customization.md
- Образец адверсарного ревью, из которого взяты вердикт, устаревание и разбор замечаний: community-схема
anvil(https://github.com/jikkujoyce/openspec-schemas/tree/main/schemas/anvil)