Skip to content

Кросс-модельное ревью замысла — 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-requirementsreview/requirements.mdproposal, specsзамысел и формулировки требований
review-designreview/design.mddesign, 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 CodeCodex CLI — scripts/codex-review.sh (поверх codex exec, read-only, без approvals) для диапазона коммитов против документа-оракула; codex review --commit <sha> для одного коммита; codex review --base main для диффа ветки
CodexClaude 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 400 invalid_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/a
  • VERDICT — ровно одно из 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 во втором случае означал бы, что закоммиченная работа ветки исчезает из проверки, а гейт объявляет ложный успех.

Что гейт проверяет ​

  1. Затронутая карточка стоит на схеме с ревью; иначе — отказ с указанием перевести её.

  2. Квитанция круга существует; Author и Reviewer заполнены, различаются и принадлежат известному множеству сторон (claude-code, codex) — произвольная строка означала бы, что различие ничего не значит.

  3. Вердикт разрешающий, APPROVE_WITH_CHANGES подтверждён, REVISE отказывает.

  4. Вердикт не противоречит собственным находкам. Проверяется механически в двух точках: открытая находка [CRITICAL] запрещает APPROVE, и число находок не должно превышать число строк разбора. Строка разбора имеет ЖЁСТКИЙ формат — начинается с «- », содержит « → » и слово «исправлено» либо «отклонено», — и считается только внутри раздела «Разбор замечаний». Формат выбран вместо markdown-таблицы после четырёх кругов ревью, каждый из которых находил новую границу грамматики таблиц (разделитель без шапки, ведущие пробелы, отступ в четыре пробела как код-блок): разбор таблиц регулярными выражениями порождает границы быстрее, чем их закрывают. Закрытость находки ПО СУЩЕСТВУ машиной не проверяется.

  5. Слепок вычисляется и совпадает с текущим содержимым. Невычислимый слепок (например, Reviewed-Paths называет несуществующий файл) — отказ, а не пропуск.

  6. Каждый затронутый файл 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 в main 2026-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 в том же диффе» действует без изменений.
  7. Круг 2 требуется, как только появился любой из design.md / tasks.md: требование обоих означало бы, что удаление одного файла отключает проверку.

  8. Изменение самой квитанции карточку из проверки не выводит — иначе подмена или удаление единственной квитанции прошли бы незамеченными.

Чего гейт НЕ доказывает ​

Он не доказывает, что ревью действительно провела вторая модель. Поля 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, и путь молча выпадал из слепка.

Известные компромиссы ​

  1. Форк замораживает инструкции штатных артефактов на версии 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.
  2. В CI шага нет. .github/workflows/parity.yml использует мелкий checkout, при котором merge-base с main недоступен и проверка прошла бы вхолостую, давая ложное «пройдено». Это третий шаг ворот, которого в CI нет, — наряду с cargo fmt --all -- --check и openspec validate --all --strict. CI и без того не запускается: у репозитория нет удалённого.

Источники ​