Конфигурация ревью
Стандарты команды живут в файле .reviewgate/config.yml в вашем репозитории — как у линтеров. Они версионируются вместе с кодом и меняются через MR/PR. Это ключевое отличие от generic-ревью: вы описываете соглашения обычным языком, а модель применяет их к коду.
version: 1
language: en # language of the findings and the summary frame (en · ru · any other)
preset: angular # the stack preset (see the table) · none
angular:
signals_naming: "camelCase, no $ prefix"
prefer: combineLatest # instead of forkJoin where applicable
zone_less: false
a11y: true # accessibility block: ARIA, keyboard, focus (off by default)
review_prompt: # your review guideline on top of the default (optional)
mode: extend # extend — add to the default · replace — replace it entirely
text: |
Pay particular attention to the resilience of HTTP calls and to leaked RxJS subscriptions.
Keep the tone short and to the point.
severity_gate: off # off (the default) | blocker | critical | major | minor | info
tests: optional # required | optional — optional does not flag missing tests
incremental_review: true # a re-push reviews only the changed files (true by default)
review_drafts: false # do not review drafts; the run happens when it is marked ready
committable_suggestions: false # ready fixes (multi-line included) as a native suggestion (1 click); better with a judge
min_severity: info # threshold for inline comments; below it they are folded into the summary (info = post everything)
rules:
- id: commit-prefix
description: "The pull request title and the commits start with TASK-<number>"
severity: critical
- id: no-any
description: "The any type is forbidden, except in *.spec.ts files"
severity: major
- id: error-handling
description: "Every HTTP call has a catchError with a typed error"
severity: major
dont_flag: # explicit team assumptions — they silence whole classes of false positives
- "Internal links do not get rel=noopener noreferrer — our convention, for analytics"
- "The empty value of our UI kit inputs is '' by convention, not null"
ignore:
- "**/*.generated.ts"
- "**/migrations/**"
integration_branches: # review only working-branch → integration pull requests (see below)
- dev
- master
- "release/**"
# Vendor keys and addresses live in the deployment environment (the LLM_BACKEND_* catalog),
# not here. config.yml holds the role layout; set it explicitly.
llm:
generators: # the reviewing models; the FIRST one is the main one
- model: claude-sonnet-4-6
effort: high # reasoning depth of the role (optional)
judges: # the judge: a second generate→verify pass (fewer false positives)
- model: claude-opus-4-8
max_context_files: 10
full_file_context: true # recall: whole files plus neighbours for the generator, not only the diff
environment_context: true # recall: stack versions (Node/TS/Angular) from the manifests into the promptИмя файла — .reviewgate/config.yml, и только оно: написание .yaml не читается. Если в репозитории появляется config.yaml, бот сообщает об этом в сводке и просит переименовать файл — политика, записанная не туда, не лежит молча.
llm задавайте явноКлючи и адреса вендоров живут в окружении инсталляции (каталог LLM_BACKEND_* и плоские LLM_*) — в config.yml их указывать нельзя. А вот раскладку ролей стоит задать явно: generators (модели-ревьюеры; первый — основной) и judges (судья, подтверждающий находки по полным файлам). Без них ревью идёт на модели по умолчанию инсталляции и одним проходом — без generate→verify, который отсекает ложные срабатывания.Поля
| поле | что задаёт |
|---|---|
| version | версия схемы конфига (сейчас 1) |
| language | язык вывода бота: и тексты ревью (замечания, итог), и каркас сводки — вердикт гейта, счётчики, подсказки, — и нотисы о деградации прогона (судейства не было, расколы панели решены без арбитра, ответ упёрся в потолок выхода). Любая строка (например en · ru · de): замечания бот пишет на указанном языке, каркас есть на английском и русском, для остальных языков каркас английский. По умолчанию en |
| preset | готовый шаблон стека (опц.). JS/TS: angular · react · vue · svelte · nextjs · nestjs · express · typescript. Python: django · fastapi · flask · python. Go: gin · echo · fiber · go. Java: spring · java. C#/.NET: aspnet · csharp. PHP: laravel · symfony · yii2 · php. Kotlin: android · ktor · kotlin. Ruby: rails · ruby. Rust: axum · actix · rust. Swift: ios · swift. Dart: flutter · dart. Также none |
| review_prompt | ваш гайдлайн ревью: mode (extend — поверх дефолта · replace — вместо него) + text |
| severity_gate | порог блокировки merge: off (по умолчанию — не блокирует) · blocker · critical · major · minor · info. Значения старой шкалы принимаются бессрочно: у ПОРОГОВ warning = major, comment = info (нижний уровень — «блокировать всё»); у rules[].severity comment = minor. Конфиги, написанные до 2.0, работают без правок |
| tests | политика тестов: required (дефолт) · optional — не отмечать отсутствие тестов |
| rules[] | правила команды свободным текстом: id, description, severity |
| dont_flag[] | явные допущения команды («это не проблема») — гасят классы ложных срабатываний; см. раздел ниже |
| ignore[] | glob-маски файлов, которые не ревьюить. Скрытые файлы бот явно объявляет модели как «есть в MR/PR, но вне ревью» — чтобы не рождались ложные «файл отсутствует». Не прячьте под ignore то, о чём есть правило (например, каталог миграций при правиле «миграция обязана быть в MR/PR») |
| integration_branches[] | ревьюить только MR/PR «рабочая-ветка → интеграционная» (см. раздел ниже); пусто — все MR/PR |
| incremental_review | ре-пуш ревьюит только изменённые файлы (по умолчанию true; false — всегда весь MR/PR) |
| review_drafts | ревьюить ли черновики (по умолчанию false — черновики пропускаются, ревью при переводе в Ready) |
| committable_suggestions | однозначные механические фиксы (в том числе многострочные, до 20 строк) — нативным suggestion-блоком (кнопка «Apply suggestion»). По умолчанию false; имеет смысл с судьёй (llm.judges) — он проверяет сам fix |
| min_severity | порог строчных комментариев: info (по умолчанию — постить всё) | minor | major | critical | blocker; старые значения порога: warning = major, comment = info (постить всё). Находки ниже порога не постятся в строки кода — показываются свёрнутым списком в сводке (список переживает инкрементальные прогоны; закрытые вами замечания не поднимаются). На severity_gate и счётчики сводки не влияет |
| cost | стоимость ревью в сводке: show (по умолчанию false) включает строки расхода — текущий прогон, ответы в тредах и итог по MR/PR; models — цены за 1 млн токенов (input/output) по имени модели, currency — метка валюты. Деньги считаются, только если цена задана для всех использованных моделей; цены — в вашей валюте, курсы валют бот не запрашивает |
| diagnostics | блок 🔬: вызовы моделей с таймингами, контекст и решения судьи с причинами — включая дропнутые находки (см. раздел ниже). Бот сворачивает его в сводке, CLI печатает в конце отчёта (в JSON — поле diagnostics). Кода в блоке нет. По умолчанию false; не задано — действует env-дефолт инсталляции DIAGNOSTICS |
| questions | жанр ❓-вопросов (см. раздел ниже): отброшенные судьёй кандидаты проходят второй взгляд по другой планке — «уместно ли сомнение», а не «доказан ли дефект», — и уместные публикуются ❓-тредами вне severity gate. По умолчанию false; требует настроенного судейства (llm.judges либо судей env-дефолтов инсталляции) |
| reply | reply mode — бот отвечает на реплики в тредах своего ревью и на @упоминания (см. раздел ниже): enabled (по умолчанию false; не задано — env-дефолт REPLY_ENABLED), model / effort / backend (по умолчанию — модель генератора), max_replies_per_thread (лимит ответов на тред, по умолчанию 3). Вебхуку нужен триггер Comments |
| llm | роли одной формы {model, backend, effort}: generators[] (модели-ревьюеры, первый — основной), judges[] (панель: 0 — без валидации, 1 — generate→verify, 2 — согласие решает, спор уходит арбитру), arbiter (председатель расколов панели); плюс max_context_files, full_file_context (полные файлы + соседи генератору, recall), environment_context (версии стека из манифестов). Ключи и адреса — в env инсталляции (каталог LLM_BACKEND_*), роль ссылается на запись по имени: backend: deepseek; backend: default — основной провайдер |
Ошибки в конфиге не проходят молча
Парсер защитный: непонятое значение не валит ревью, а откатывается к дефолту. Но об откате он сообщает — строкой в сводке и в поле run.config.notices отчёта CLI. Названы там: опечатка в ключе (sevirity_gate — гейт остался бы выключенным, пока команда считает блокировку настроенной), значение не той формы (rules мэппингом вместо списка, ignore строкой, review_prompt строкой вместо объекта), а также нераспознанные preset, severity_gate, tests, min_severity, rules[].severity и effort с backend у роли. Последние два важнее прочих: отброшенный backend отправляет запрос провайдеру по умолчанию — тому, которого команда не выбирала, — а правило с непонятым severity откатывается к major, и правило, задуманное блокирующим, перестаёт держать гейт.
Проверка идёт глубже верхнего уровня. Лишний ключ внутри известного блока тоже объявляется: правило с globs или prompt (схема правила закрыта — id, description, severity; scoping и условия живут в review_prompt), provider внутри роли, опечатка внутри reply или cost. Объявляются и булево, которое булевым не является (questions: enable), и блок review_prompt без text, и посторонний config.yaml — бот читает только config.yml и говорит об этом.
То же самое с файлом целиком. Если он есть, но бот не смог его прочитать — у токена нет прав на репозиторий, не поднялась сеть до репозитория, — ревью всё равно пройдёт, но по дефолтам: без правил команды, без ignore, без гейта. У этого случая теперь своя строка в сводке, и лечение в ней названо точно, потому что оно противоположно случаю битого файла: проверять надо права токена бота, а не YAML.
llm.api_key, base_url, proxy здесь не применяются: связь и секреты настраиваются окружением инсталляции — иначе клон чужого репозитория уводил бы ваш код на чужой адрес. Если ключ всё-таки попал в файл, мы скажем об этом прямо: файл лежит в git, значит ключ уже в истории и его надо отозвать.Валидация до коммита — JSON Schema
Всё перечисленное выше происходит во время ревью. Те же ошибки можно поймать до коммита: схема конфига опубликована как JSON Schema по адресу reviewgate.dev/config.schema.json. Укажите её редактору комментарием из первой строки ниже — YAML-расширение VS Code (yaml-language-server) валидирует и подсказывает прямо при наборе. Тот же файл работает в CI и в руках ИИ-агента, который пишет конфиг за вас: неизвестные ключи, закрытая схема правил, форма ролей, капы — схема зеркалит парсер и держится с ним в синхроне тестами.
# yaml-language-server: $schema=https://reviewgate.dev/config.schema.json
version: 1
# … the rest of the configПолный пример — все опции
Все поля разом, для справки. Всё опционально (парсер защитный: неизвестное и невалидное заменяется дефолтами, о невалидном бот предупреждает в своих логах), кроме блока llm, который стоит задавать явно (см. выше).
# The full .reviewgate/config.yml schema — every field, for reference.
# The parser is defensive: anything unknown or invalid is replaced by a default (and the bot
# warns about the invalid parts). Most fields are optional; set the llm block explicitly.
version: 1 # schema version (currently 1)
language: en # language of the findings and the summary frame (en · ru · any other)
preset: angular # the stack preset (see the table) · none — no preset
angular: # options of the chosen preset (the key is the preset name)
signals_naming: "camelCase, no $ prefix"
prefer: combineLatest
zone_less: false
a11y: true # optional: accessibility checks (ARIA / keyboard / focus)
review_prompt: # your own review guideline (optional)
mode: extend # extend — on top of the default · replace — entirely instead of it
text: |
The guideline text in any language — what to check, the tone, the domain.
severity_gate: off # off | blocker | critical | major | minor | info — the merge threshold
tests: required # required | optional — optional does not flag missing tests
incremental_review: true # a re-push reviews only the changed files (saves tokens)
review_drafts: false # whether to review drafts (no by default)
committable_suggestions: false # fixes as a native suggestion block (1 click); better with a judge (llm.judges)
min_severity: info # blocker | critical | major | minor | info — threshold for inline comments
rules: # your team rules in plain English
- id: error-handling
description: "Every HTTP call handles its error (ADR-003)"
severity: major # blocker | critical | major | minor | info
- id: mr-description # request variables: {mr:author} {mr:title} {mr:description}
description: "If the request has no description — ask {mr:author} to add one" # {mr:source_branch}
severity: minor # {mr:target_branch}
# {mr:url} {mr:project}
dont_flag: # team assumptions — «not a problem, do not flag» (silences false positives)
- "Internal links without rel=noreferrer — our convention"
ignore: # glob masks of files outside the review
- "**/*.generated.ts"
integration_branches: # review only working → integration (empty — every request)
- dev
- master
diagnostics: false # the 🔬 block in the summary: calls, timings, judge decisions with reasons
# (no code in the block; unset — the DIAGNOSTICS env default)
reply: # reply mode: the bot answers replies in threads (see the section)
enabled: true # unset — the REPLY_ENABLED env default; the webhook needs the Comments trigger
model: claude-sonnet-4-6 # the model for replies (optional; the generator model by default)
effort: high # reasoning depth (optional)
backend: deepseek # a named backend from the deployment environment (optional, multi-vendor)
max_replies_per_thread: 3 # cap on the bot's replies per thread (anti-loop)
cost: # the cost of the review in the summary (off by default)
show: true # spend lines in the summary: this run, thread replies, the total
currency: "$" # the currency label ($ by default); prices are given in it — the bot fetches no rates
models: # prices per 1 MILLION tokens; the key is the model name EXACTLY as in llm/env
# prices are needed for EVERY model of the run: generators, judges and the arbiter.
# The numbers below are an illustration; take the current ones from YOUR provider's price list.
claude-sonnet-4-6: { input: 3, output: 15 }
claude-opus-4-8: { input: 5, output: 25 }
claude-fable-5: { input: 10, output: 50 }
deepseek-chat: { input: 0.3, output: 1.2 }
gpt-4o: { input: 2.5, output: 10 }
# Vendor keys and addresses live in the deployment environment (the LLM_BACKEND_* catalog),
# NOT in config.yml. Every role has the same shape {model, backend, effort}; backend is the name
# of a catalog entry (default = the deployment's main provider), and an unset effort falls back
# to the deployment default.
llm:
generators: # the reviewing models; the FIRST is the main one (replies, fallbacks)
- model: claude-sonnet-4-6
effort: high
- model: claude-opus-4-8 # another pair of eyes — recall goes up
- backend: deepseek # a role on a named backend from the environment (multi-vendor)
judges: # the panel: 0 — no validation; 1 — generate→verify;
- model: claude-opus-4-8 # 2 — agreement decides, a split goes to the arbiter (cap 2)
arbiter: # the chairman: resolves splits of a two-judge panel
model: claude-fable-5 # (never called without judges)
max_context_files: 10 # how many diff files to keep in context
full_file_context: true # recall: whole files plus imported neighbours for the generator
environment_context: true # recall: stack versions from the manifests into the promptКак составить конфиг с нуля
Порядок, который даёт рабочий конфиг без догадок:
- Стек → preset. Стек есть в таблице пресетов выше? Возьмите его как основу. Нет в списке —
preset: noneи опишите ревью вreview_prompt. - Правила — из того, что уже есть. Пройдитесь по линтерам (
.eslintrc,phpcs.xml…) и канону команды (ADR, CONTRIBUTING, wiki). Вrules[]выносите только то, что линтер НЕ ловит — договорённости команды; сошлитесь на источник в тексте правила. Дублировать линтер — шум. - Ветки. Задайте
integration_branches(см. раздел ниже) — иначе бот ревьюит все MR/PR подряд, включая служебные merge. - Тон под домен. Нужен акцент — добавьте
review_prompt(extendповерх пресета илиreplaceцеликом). - Точность vs стоимость. Нужна высокая точность — добавьте судью:
llm.judges: [{ model: … }](2-й проход generate→verify, см. LLM). Поток дешёвых ревью — один генератор без судей. - Максимальное качество (ансамбль, опц.). Для «идеального кода по вашим правилам» — несколько независимых генераторов:
llm.generatorsперечисляет модели-ревьюеры (каждая ищет самостоятельно, разные модели видят разное — растёт recall), судья судит объединённые находки, схлопывает дубли и отсеивает ложные (держит точность). Второй судья превращает судейство в панель: согласие — решение, спор решаетllm.arbiter— председатель. Каждая роль — дополнительный платный проход; включайте под самую высокую планку. Роль можно увести на другого вендора —backend: имяиз каталога инсталляции (например, DeepSeek вторым генератором), см. LLM → мультивендор. - Глубина поиска (recall). Чтобы бот видел не только дифф, а полные изменённые файлы и их импортируемых соседей —
llm.full_file_context: true(ловит проблемы вне ханка, не домысливая). Чтобы судил по версиям стека (Node/TS/Angular из манифестов) —llm.environment_context: true(не путает доступность API в вашей версии). Оба opt-in и дороже по токенам — включайте под высокую планку качества, лучше вместе с судьёй. - Фиксы в один клик (опц.). Хотите, чтобы бот предлагал однозначные правки нативным suggestion-блоком (кнопка «Apply suggestion») —
committable_suggestions: true. Замена может покрывать и несколько соседних строк диффа (до 20). Применяет код прямо в ветку, поэтому держите вместе с судьёй (llm.judges— он проверяет сам фикс и его диапазон) и обкатайте сначала на доверенных репозиториях. - Меньше шума (опц.). Если мелкие замечания отвлекают —
min_severity: major: ниже порога бот не постит строчных комментариев, находки остаются свёрнутым списком в сводке (ничего не теряется). На блокировку (severity_gate) порог не влияет. - Обкатайте без блокировки. Оставьте
severity_gate: offна старте, посмотрите находки на нескольких MR/PR, и только потом включайте порог.
preset + extend или none + replace?
- preset (+ опц. extend) — стек в списке, правила обычные. Готовая база, точечные акценты. Самый быстрый путь.
- none + replace — стека нет в пресетах или у вас сильный собственный гайдлайн / ADR, который хотите контролировать дословно. Пишете промпт целиком — формат вывода бот добавит сам.
Правила команды
rules[] — это и есть отличие от линтера. Каждое правило: id (стабильный идентификатор), description (соглашение обычным текстом) и severity. Описание правила с тем же id может переопределить severity находки.
Находки по вашим правилам помечаются бейджем 📐 — и в строчном комментарии, и в сводке (строка «по правилам команды»). Так видно, что замечание опирается на ваш config.yml, а не на общие соображения модели; судья второго прохода дополнительно сверяет такие находки с текстом самого правила и отбрасывает те, что держатся только на ложной привязке. Правило с опечаткой в полях (id/description) не применяется — бот скажет об этом строкой в сводке и WARN в логе.
Исключить один файл: маркер внутри файла
Ключ ignore — про класс файлов: генерируемое, вендоренное, миграции; класс называется одним glob-выражением и не растёт. Одиночный файл — другая история. Запись в списке никогда не сокращается, молча ломается при переносе файла и провоцирует широкий glob, который годами глотает новые файлы. Для такого случая маркер ставится в сам файл:
// reviewgate-ignore-file: generated test cases, a review adds no signalМаркер обязан начинать строку и стоять за комментарием. Распознаются формы большинства языков: семейство слэшей вместе с doc-комментарием Rust, решётка, двойной дефис, звёздочка, блочные и JSX-комментарии, HTML, точка с запятой, процент, апостроф, скобки OCaml и Haskell, формы batch. Действует в любом месте файла, не только в шапке. Причина обязательна: маркер без причины не применяется, файл ревьюится как обычно, и сводка об этом говорит — иначе через год никто не восстановит, зачем повесили намордник. Если строка похожа на маркер, но форма не распознана, сводка скажет и об этом: маркер не отказывает молча.
Исключённый так файл до модели не доезжает: ни его дифф, ни содержимое, ни даже подтягивание соседом по импорту чужой находки — поэтому его размер не стоит вам токенов. Сводка перечисляет все снятые файлы вместе с причинами и отдельно помечает те, чей маркер добавлен в этом же запросе: правка и намордник на неё, приехавшие вместе, не должны проходить незамеченными.
Ложное срабатывание: как погасить класс
Если бот систематически флагает то, что для вашей команды норма — внутренняя конвенция, осознанный приём, — запишите допущение в dont_flag обычной фразой. Допущение видят все этапы — генератор находок, судья второго прохода и ответы в тредах, — поэтому класс гасится целиком, а не по одному замечанию. Формулируйте узко, про конкретный приём: слишком широкое допущение («не флагайте обработку ошибок») погасит и настоящие проблемы.
dont_flag:
- "Internal links do not get rel=noopener noreferrer — our convention, for referrer analytics"
- "The empty value of our UI kit inputs is '' by convention, not null/undefined"
- "console.log is fine under scripts/ — those are CLI utilities"Тот же эффект даёт исключение прямо в тексте правила («…; исключение: допустимо в тестах») — dont_flag нужен там, где отдельного правила нет. Встроенная дисциплина и без настройки не флагает вкусовщину вне ваших стандартов, микрооптимизации без измеримого эффекта, осознанные подавления с пояснением (eslint-disable, TODO с тикетом) и «заодно бы ещё» вне цели изменения.
Свой промпт ревью
Бот ревьюит по системному промпту — и этот промпт под вашим контролем. Из коробки работает разумный дефолт (что искать: баги, отказоустойчивость, безопасность, производительность, типизация), а через review_prompt вы пишете гайдлайн под свой проект, домен и стек — на любом языке, не только из списка пресетов.
| слой промпта | кто им владеет |
|---|---|
| гайдлайн ревью — что искать, тон, домен | вы — в review_prompt (или разумный дефолт) |
| формат вывода + дисциплина против ложных срабатываний | бот — машинный контракт, переопределить нельзя |
mode: extend — ваш текст добавляется к дефолтному гайдлайну (точечно усилить акценты). mode: replace — дефолт не подмешивается, ревью идёт целиком по вашему промпту:
review_prompt:
mode: replace # a guideline entirely your own — the default is not mixed in
text: |
You are reviewing an Angular project. What to look at:
— Resilience: HTTP calls without catchError; subscriptions never unsubscribed.
— Security: innerHTML without sanitising, secrets in the code.
— List states: loading / error / empty result.
— Typing: any where a concrete type can be inferred.
Keep the tone short and to the point.
preset: none # your own prompt is self-sufficient — no preset neededМиграция со схемы 1.x
В 2.0 роли LLM записываются единообразно — списками ролей одной формы {model, backend, effort}. Старые ключи сняты: бот не применяет их и отвечает на каждый заметным notice в сводке с готовой строкой замены (ревью при этом выполняется — на канонических настройках и дефолтах инсталляции).
| было (1.x) | стало (2.0) |
|---|---|
llm.model: X | llm.generators: [{model: X}] |
llm.effort | effort в роли генератора |
llm.validate_model: X | llm.judges: [{model: X}] |
llm.validate_effort | effort в роли судьи |
llm.extra_generators | роли в llm.generators (основной — первым) |
llm.provider | вендор выбирается бэкендом роли (backend: имя) |
llm.arbiter при ансамбле (1.x: судья объединённых находок) | llm.judges: [{model: X}] — роль «судит и схлопывает union» в 2.0 играет судья; арбитр 2.0 — председатель расколов панели |
арбитр «дорастал» из validate_model | наследования нет: судит панель judges, арбитр — председатель расколов |
Переносить руками не обязательно: reviewgate migrate делает это сам — по умолчанию показывает предпросмотр, --write применяет с бэкапом рядом. Команда понимает конфиг репозитория, личный конфиг CLI и .env-файлы инсталляции; секреты не печатает и не трогает, комментарии YAML сохраняет. Подробно: reviewgate help migrate и справочник CLI.
Поведение раскладки — «лестница»: один генератор без судей — законный эконом-режим; несколько генераторов без судей — union с механическим дедупом и notice с рецептом (семантические дубли никто не схлопнет); арбитр зовётся только при расколе панели из двух судей — без судей или при панели из одного он не зовётся, о чём прогон говорит notice. Явный judges: [] — осознанное «без валидации», он гасит и дефолт инсталляции. Рецепт «только основной провайдер инсталляции, без env-ансамбля»: generators: {backend: default}.
Расколы панели из двух судей решает арбитр-председатель; без него исход консервативный и зависит от вида спора: спор о правде находки — она отбрасывается (точность важнее полноты), спор о том, дубликат ли она другой находки, — сохраняется (лишний дубль дешевле потерянной находки), но committable-фикс у спорной находки снимается. Исключение — когда судьи указывают друг на друга (первый считает дублем одну находку, второй — другую): про сам факт дублирования они согласны и спорят лишь о том, кого оставить, поэтому остаётся одна. Все исходы видны в диагностике прогона.
Переменные MR/PR в промптах
В rules[].description и review_prompt можно ссылаться на метаданные конкретного MR/PR — плейсхолдерами вида {mr:поле}. Классика жанра — правило про гигиену MR/PR:
rules:
- id: mr-description-required
description: "If the request has no description or no linked task — mention {mr:author} and ask for one"
severity: minorБот подставит настоящий ник: в MR/PR без описания появится замечание вида «@ivanov, добавьте описание: что меняется и зачем». Автору придёт обычное уведомление платформы об упоминании.
| переменная | значение |
|---|---|
{mr:author} | ник автора MR/PR (бот упоминает через @) |
{mr:title} | заголовок MR/PR |
{mr:description} | описание MR/PR (пустое бот видит как «(пусто)») |
{mr:source_branch} / {mr:target_branch} | ветки MR/PR |
{mr:url} | ссылка на MR/PR |
{mr:project} | путь проекта (группа/репозиторий) |
Severity gate
По умолчанию severity_gate: off — бот ничего не блокирует, только оставляет комментарии. Ревью помогает изменению доехать, а не мешает.
Если команде нужен жёсткий порог, поставьте severity_gate: critical (или major). Тогда при находках этого уровня и выше бот выставляет неуспешный статус — check run на GitHub, commit status на GitLab, — и при настройке «pipeline must succeed» или обязательной проверки это блокирует merge.
Порог считается не только по последнему прогону. Если замечание уровня порога уже висит открытым тредом, а очередной прогон его файл не перепроверял (так бывает при инкрементальном ревью: изменился только другой файл) — оно продолжает держать статус красным, и в сводке это сказано прямо: «из них N — ранее открытые». Иначе посторонний коммит гасил бы блокировку, а замечание оставалось бы неисправленным.
Снять блокировку можно двумя способами: исправить — тогда следующий прогон перепроверит файл, не найдёт замечания и статус позеленеет сам; либо закрыть тред (Resolve) — это ваш сигнал «учтено или не относится к делу». Специально резолвить исправленное не нужно.
Какие MR/PR ревьюит бот
Без integration_branches бот ревьюит каждый MR/PR. Обычно это лишнее: служебные merge (доставка dev → release → master, back-merge, синки) ревью не требуют и создают шум. Список интеграционных веток включает фильтр:
integration_branches:
- dev
- master
- "release/**"
# A review runs when the source branch is NOT in the list and the target IS (feature/X -> dev, say).
# A request between two branches from the list (dev -> master, a back-merge) is skipped as internal.feature/* у вас — обычная рабочая ветка под одну задачу (вливается в dev), не включайте её: иначе MR/PR feature/… → dev бот примет за служебный и молча пропустит. Добавляйте feature/*, только если это ветки-агрегаты, в которые вливаются другие.Повторные ревью и Resolve
При новом пуше бот ревьюит заново, но не засыпает MR/PR дублями: уже открытые замечания не повторяются, а сводка одна и обновляется на месте. Если вы нажали Resolve на треде бота — это сигнал «учтено / не релевантно»: при следующих ревью бот не поднимет это замечание снова.
Повторное ревью инкрементальное: бот проверяет только файлы, изменившиеся с прошлого ревью, а не весь дифф — это экономит токены. Поведение по умолчанию; чтобы всегда ревьюить всё изменение целиком, поставьте incremental_review: false.
Черновики (Draft) бот по умолчанию не ревьюит — прогоняет их один раз, когда MR/PR переводят в Ready. Чтобы ревьюить и черновики, поставьте review_drafts: true.
Стоимость ревью в сводке
С cost.show: true бот дописывает в сводку расход — сколько токенов ушло и, если задан прайс, во сколько это обошлось на вашем ключе. Строк до трёх: текущий прогон, ответы бота в тредах и итог по всему MR/PR:
⚙️ This review: 46.2K tokens in · 3.8K out · ≈ 0.21 $
💬 Thread replies (3 replies): ≥ 12.1K tokens in · 0.9K out · ≥ 0.05 $
🧾 Total for this pull request (2 runs + 3 replies): ≥ 1.2M tokens in · 12.4K out · ≥ 1.32 $language: en. С language: ru бот пишет тот же вывод по-русски, а разделителем дробной части становится запятая: ≈ 0,21 $ вместо ≈ 0.21 $. Это относится ко всему выводу: строкам стоимости, блоку «🔬 Диагностика прогона», предупреждениям о потолке трат и причинам, по которым судья снял кандидата.Сводка обновляется на месте, поэтому строка прогона всегда про последний прогон — а итог копится по MR/PR и переживает как повторные ревью, так и упавшие прогоны. В скобках указан охват («2 прогона + 3 ответа»): учёт начинается с того прогона, когда бот впервые записал итог в этот MR/PR, — то, что было потрачено раньше, в сумму не попадёт.
cost:
show: true # false by default
currency: "$" # the currency label (prices are given in it)
models: # prices per 1 MILLION tokens; the key is the model name (llm block or env)
claude-sonnet-4-6: { input: 3, output: 15 }
claude-opus-4-8: { input: 5, output: 25 }
# prompt cache prices are optional: without them the vendor multipliers are used
# (writes 1.25x at a 5m TTL and 2x at an hour, reads 0.1x of input)
# claude-sonnet-4-6: { input: 3, output: 15, cache_read: 0.3 }Цены задаются за 1 млн токенов в вашей валюте — по тарифам вашего провайдера. Бот работает офлайн и курсы валют не запрашивает, поэтому конвертации нет: что записали, в том и посчитает. Считаются все вызовы прогона — генератор, дополнительные генераторы ансамбля, судья и арбитр, каждый по цене своей модели.
Как заполнить цены — три шага:
- Возьмите актуальный прайс своего провайдера.
- Приведите цены к «за 1 млн токенов»: Anthropic, OpenAI и DeepSeek публикуют их сразу за 1 млн — берите как есть; Yandex AI Studio — за 1 тысячу токенов, умножьте на 1000.
- Если провайдер биллит не в валюте
currency— умножьте на курс, по которому вы реально платите (карта, посредник, договор), и запишите результат. Бот сам ничего не конвертирует.
# The numbers are an illustration; take the current prices from your provider's price list.
# Keep ALL prices in the config in one currency — the one given in currency.
# Anthropic and the OpenAI-compatible vendors publish prices in $ per 1 MILLION tokens — copy them as is:
claude-sonnet-4-6: { input: 3, output: 15 }
# DeepSeek publishes prices in ¥ (CNY) and in $. If you pay in yuan, set currency: "¥"
# and give the prices in ¥ per 1 MILLION tokens:
deepseek-chat: { input: 2, output: 8 }
# Yandex AI Studio bills in ₽ but publishes the price per 1 THOUSAND tokens: multiply by 1000
# (and set currency: "₽"). The model name is the full URI, quoted because of the «://»:
"gpt://<folder>/qwen3-235b-a22b-fp8/latest": { input: 40, output: 120 } # 0.04 / 0.12 ₽ per 1K × 1000
# If you pay in a different currency (a card, an intermediary) — convert at YOUR OWN rate.cost.models должно совпадать дословно с тем, что задано в ролях (llm.generators / judges / arbiter) либо в env (LLM_MODEL). Для Yandex AI Studio это полный URI вида gpt://<folder>/… — в YAML берите его в кавычки. Не совпало — бот честно напишет «нет цены для: …» вместо суммы.LLM_CACHE_TTL=5m и 2× при часе, чтение — 0,1×. Множители применяются к вашему input автоматически; переопределить их можно полями cache_write_5m, cache_write_1h, cache_read — они необязательны, и конфиги только с input/output продолжают работать. В строке прогона видно, сколько входа пришло из кэша. У OpenAI-совместимых провайдеров кэшированные токены уже входят в prompt_tokens, поэтому второй раз не считаются. После обновления бота итог по уже открытым MR/PR может сочетать прогоны, посчитанные ещё без кэша, с новыми — такие итоги подрастают по мере новых прогонов; задним числом прошлые прогоны не пересчитываются.Итог помечается «≥» и тогда, когда часть расхода заведомо не поддаётся учёту: прогон упал уже после ответа модели (токены списаны, объём неизвестен), либо включён режим ответов в тредах — отвечая, бот вправе промолчать, а молчаливый вызов оплачен и следа в MR/PR не оставляет. Знак «≥» значит «не меньше этого», и никогда — «примерно столько».
Диагностика прогона
С diagnostics: true бот дописывает в сводку свёрнутый блок 🔬 — объяснение своих решений прямо там, где живёт разработчик: не нужен доступ к серверу бота, логам или админу. Тот же блок печатает reviewgate review в конце отчёта (в JSON — поле diagnostics, его же видит агент через MCP). Внутри — вызовы моделей с расходом и таймингами, собранный контекст и, главное, решения судьи с причинами, включая дропнутые находки — то, что без этого блока не видно никому:
🔬 Run diagnostics
**Model context**: diff: 14 files · full files: 14 (191K chars) · environment: ✓ · from team standards (12 rules): 2 of 5 findings
**Calls**:
- generator `claude-sonnet-4-6` — 41.3K→2.9K tokens · 38 s · findings: 5
- validator `claude-opus-4-8` — 96K→1.1K tokens · 64 s · dropped: 1 · downgraded: 1
**The judge dropped / downgraded / removed a fix (✂️)**:
- ❌ `cart.service.ts:42` `error-handling` — «catchError is already one line above — the finding is false»
- ⬇️ `api.ts:90` `sql-injection` critical→major — «the input is already parameterised, the risk is indirect»Частые вопросы, которые блок закрывает сам: «почему бот не ругнулся на X» — судья дропнул, причина в блоке; «правило написали, а оно молчит» — виден весь путь прогона; «почему ревью шло две минуты» — тайминги по вызовам. Если config.yml в репозитории не найден или невалиден, блок честно скажет «работаю на дефолтах» — ответ на самый частый вопрос при настройке.
Одного не нужно ждать от diagnostics: true: если сам прогон вышел мельче заказанного — ответ модели упёрся в потолок выхода и был повторён с пониженным усилием, модель вовсе не поддержала режим рассуждений, дополнительный генератор ансамбля упал или не был настроен, — сводка скажет об этом сама, свёрнутой строкой со счётчиком. Знать там стоит две вещи: анализ не тот, что вы просили, и обрезанный ответ оплачен дважды.
DIAGNOSTICS=true (удобно на время онбординга, пока config.yml ещё не создан); diagnostics: false в конфиге репозитория выключает её точечно.❓ Вопросы к автору
Судья требует доказанного дефекта — именно это держит точность ревью. Но часть отброшенного — легитимное инженерное сомнение, у которого сегодня нет жертвы: клип на контейнере, прячущий симптом вместо причины, проглоченная ошибка на живом пути. С questions: true такие отказы проходят второй взгляд судьи прогона (первого из настроенной панели) по другой планке — «уместно ли сомнение», а не «доказан ли дефект», — и уместные возвращаются вопросами: сформулированы вопросом и называют, что снимет сомнение.
Вопрос — не замечание: он публикуется отдельным ❓-тредом, не входит в severity gate, порог min_severity и счётчики замечаний, а сводка считает его отдельной строкой. Ответ в треде работает как обычно (reply mode); после resolve автора повторные прогоны вопрос не переспрашивают. На проектах с обязательным resolve всех тредов платформа сама попросит ответить на ❓-тред до мержа — гейт вопрос не трогает, но ответа он ждёт. В отчёте CLI и MCP вопросы приходят массивом questions[] — для агента это прямое задание: проверить сомнение по коду и либо поправить правку, либо ответить. Дубликаты не переспрашиваются, линзе предписано не задавать вопросов по темам из dont_flag, а сбой линзы не трогает замечания — прогон честно говорит об этом, а не молчит. По умолчанию выключено; требует настроенного судейства (llm.judges либо судей env-дефолтов инсталляции) — без судьи нет отказов для переквалификации, и сводка об этом скажет.
Reply mode — ответы в тредах
С reply.enabled: true бот участвует в обсуждении: разработчик пишет реплику в тред замечания («а почему это утечка?») — и бот отвечает в том же треде, опираясь на код и стандарты команды из этого же конфига. Ответ приходит сам, обычно за десятки секунд — не нужен запуск джобы в CI или чья-то команда.
reply:
enabled: true # off by default (or the REPLY_ENABLED env default)
max_replies_per_thread: 3 # past the cap the bot stays silent in that thread
# model / effort / backend are optional; by default the generator model answers (llm.model)Когда бот отвечает:
- на реплики людей в тредах своего ревью — строчных замечаниях и под сводкой;
- на явное @упоминание учётки бота в любом другом треде MR/PR;
- в тредах черновика — да (реплика — явный вопрос); в закрытых (resolved) тредах — нет.
Бот не отвечает ради ответа: реплики «поправил», «ок, спасибо» он распознаёт и молчит — токены не тратятся на вежливость. Если бот признаёт замечание ложным, он прямо это пишет и предлагает закрыть тред — но резолвит тред всегда человек: никаких действий с кодом или статусами бот от реплик не выполняет.
diagnose.sh --project проверяет это автоматически.reply.model (и backend — на другого вендора). Лимит max_replies_per_thread (по умолчанию 3) страхует от пинг-понга двух ботов и бесконечного спора: после лимита бот в треде молчит. Включается и на всю инсталляцию env-переменной REPLY_ENABLED=true; reply.enabled: false в конфиге репозитория выключает точечно.Пресеты стека
Пресеты — готовые шаблоны гайдлайна для частых стеков: удобная отправная точка, чтобы не описывать всё в review_prompt с нуля. Они опциональны; с review_prompt: replace и preset: none вы целиком на своём промпте.
Для JS/TS пресеты слоёные: общее языковое ядро (типы, async/промисы, обработка ошибок, утечки, безопасность) + специфика фреймворка поверх. Поэтому react, vue, nestjs и т. д. уже включают TS-ядро — отдельно его указывать не нужно.
| preset | что проверяет · опции |
|---|---|
| angular | Signals (computed/effect+untracked, resource()/httpResource(), сигнальные queries, чтение за await), RxJS (switchMap, toSignal), control flow (@if/@for+track, @defer, @let), DI (inject() вне контекста, scope сервисов), маршрутизация (guard/резолверы/порядок маршрутов), формы (ngModel+name, markAllAsTouched), стили (::ng-deep, инкапсуляция), OnPush/zoneless, NgOptimizedImage, строгие формы/шаблоны, запрет any, версионная дисциплина (Angular N+). Опции: signals_naming, prefer, zone_less, a11y (проверки доступности: ARIA-состояние, клавиатура, фокус — по умолчанию выключены; true включает) |
| react | хуки (Rules of Hooks, deps/stale closure, бесконечный рендер), гонки и очистка эффектов (AbortController), производное состояние и key, мемоизация, dangerouslySetInnerHTML. Опция: style |
| nextjs | App Router поверх React: границы "use client", утечка серверных секретов в бандл, валидация и авторизация в server actions, кэш/revalidate, next/image |
| vue | реактивность (потеря при деструктуризации, toRefs, ref/reactive), очистка watch и гонки async, computed vs метод, v-for key, v-html |
| svelte | Svelte 5 runes ($state/$derived/$effect, очистка эффектов), потеря реактивности при деструктуризации, stores, ключи в {#each}, {@html} |
| nestjs | DI/scope (request-scoped в singleton), DTO + ValidationPipe, exception filters вместо голого 500, транзакции и N+1, guards на изменяющих эндпоинтах, ClassSerializer |
| express | async-роут без catch → unhandled rejection, порядок middleware, валидация входа + helmet/CORS/rate-limit, open redirect, стримы. Опция: framework: express | fastify |
| typescript | голое языковое ядро JS/TS без фреймворка (типы, async/промисы, ошибки, утечки, безопасность) — для Node/скриптов |
| django | ORM N+1 (select_related/prefetch_related), запросы без пагинации, transaction.atomic, валидация форм, SECRET_KEY/DEBUG/mark_safe, права на view, миграции с данными |
| fastapi | Pydantic-контракты и response_model, блокирующие вызовы в async-эндпоинте, Depends/yield для ресурсов, авторизация в зависимостях, секреты через Settings |
| flask | контекст app/request/g, глобальное состояние между запросами, блокирующий обработчик, валидация входа, CSRF, debug/секреты в проде |
| python | голое ядро Python без фреймворка (мутабельные дефолты, asyncio-ловушки, except/ресурсы, eval/pickle/SQL-инъекции, GIL) |
| gin | биндинг+валидация (ShouldBindJSON), c.Abort после ошибки, c.Copy для горутин, recovery-middleware, авторизация |
| echo | c.Bind + валидатор, возврат error через HTTPErrorHandler, контекст не для горутин, middleware авторизации |
| fiber | fasthttp: ctx/Locals невалидны после хендлера (не держать, не в горутины), BodyParser+валидатор, ErrorHandler |
| go | голое ядро Go (goroutine leak/deadlock/гонки, context-отмена, error-wrapping %w/errors.Is, defer/ресурсы, typed nil, crypto/rand) |
| spring | Spring Boot/MVC/JPA: @Transactional через прокси (self-invocation, rollbackFor), JPA N+1/LazyInit, гонки в синглтон-бинах, @Valid/DTO, @PreAuthorize, секреты |
| java | голое ядро Java (equals/hashCode, == vs equals и Integer-кеш, Optional, try-with-resources, потокобезопасность, SecureRandom) |
| aspnet | ASP.NET Core + EF Core: captive dependency (Scoped в Singleton), DbContext не потокобезопасен, EF N+1/AsNoTracking, DTO/over-posting, [Authorize], секреты |
| csharp | голое ядро C#/.NET (async void / .Result-deadlock / ConfigureAwait, IDisposable/HttpClient, nullable refs, LINQ multiple-enumeration, throw;) |
| php | ядро PHP 8.3: type juggling (==/=== и 0e-хеши), unserialize/object injection, XSS/command injection/path traversal, random_bytes, strict_types/readonly, SQL-инъекции, N+1, RabbitMQ/Redis. Опция: framework |
| laravel | Eloquent N+1, $fillable/mass assignment, FormRequest-валидация, Policy/Gate, API Resource/DTO, Blade {!! !!} XSS, Jobs, config()/env |
| symfony | autowiring/приватные сервисы, Doctrine N+1 и flush в цикле, Constraints+DTO, Voters/#[IsGranted]/CSRF, Twig |raw, Messenger |
| yii2 | ActiveRecord сценарии/safe-атрибуты, RBAC, N+1 (with/joinWith), тонкие контроллеры, параметризация/Html::encode |
| kotlin | голое ядро Kotlin (корутины: runBlocking/GlobalScope/Dispatchers/кооперативная отмена, null safety !!/lateinit, when-exhaustive/scope-функции/val, SecureRandom) |
| android | lifecycleScope/viewModelScope, утечки Context/View, Jetpack Compose (LaunchedEffect/remember/stable, ключи LazyColumn), main-thread/ANR |
| ktor | структурные корутины/таймауты, ContentNegotiation-валидация, StatusPages, переиспользование HttpClient, Authentication |
| ruby | голое ядро Ruby (nil &. и 0-truthy, rescue StandardError vs Exception, мутабельность строк/proc-vs-lambda, eval/send/Marshal/YAML.load, command injection) |
| rails | ActiveRecord N+1 (includes/preload), SQL-инъекция (where-интерполяция), strong params/mass assignment, Pundit/before_action, html_safe/raw XSS + CSRF, callbacks-побочки, ActiveJob |
| rust | голое ядро Rust (unwrap/expect-паники, ?/thiserror, lock через .await, clone-избыточность, unsafe, as-касты/overflow) |
| axum | lock через await / spawn_blocking, extractors-валидация, IntoResponse для ошибок, без unwrap в хендлере, auth/таймауты (tower) |
| actix | web::block для блокирующего, web::Data (состояние), extractors-валидация, ResponseError, auth/лимиты payload |
| swift | голое ядро Swift (force unwrap !/try!, retain cycles [weak self], @MainActor/Sendable, struct vs class, try? проглатывание, Keychain) |
| ios | SwiftUI @StateObject vs @ObservedObject, чистый body/.task, стабильные id в ForEach, main-thread/@MainActor, retain cycles, Keychain |
| flutter | iOS+Android: dispose контроллеров/StreamSubscription, BuildContext через async gap (mounted), setState после dispose, const-виджеты/ListView.builder/Key, состояние (Provider/Riverpod/Bloc) |
| dart | голое ядро Dart (null safety !/late, Future без await / Stream cancel / StreamController close, const/pattern matching, Random.secure) |
review_prompt или правилами rules: пресет не обязателен.