Первая версия нашего AI-ревьюера отметила 14 «критических» проблем в однострочном изменении конфига. Двенадцать были вымышленными. Одна — реальной опечаткой. И одна — настоящим багом, который больше никто не заметил. Именно это соотношение — один сигнал, погребённый под двенадцатью выдумками, — и есть причина, по которой большинство команд выключает бота за неделю и больше не включает.
Нам не нужен был такой бот. Нам был нужен второй ревьюер, который никогда не устаёт, но и не выдумывает проблему из воздуха. Ревьюер, который запускает наш настоящий набор тестов, а не угадывает его, комментирует один раз тем, что нашёл, и стоит предсказуемых денег. Это та конфигурация, которая после нескольких неловких итераций реально выдержала в нашем CI.
Всё здесь работает на открытом коде: Octomind — сессионный агент, и octomind-action — GitHub Action, который его оборачивает. Без SaaS-ревьюера, без оплаты за место, без того чтобы код покидал ваш раннер куда-либо, кроме провайдера модели, за которого вы и так платите.
Почему «самохостинговый AI-ревьюер», а не SaaS-бот
Сейчас есть здоровый рынок хостинговых продуктов AI-ревью кода. Они работают. Но для AI-ревью кода в CI три вещи подтолкнули нас к самохостинговому AI-ревьюеру, работающему внутри наших собственных GitHub Actions:
- Ревьюер должен запускать наши команды, а не своё представление о наших командах. Хостинговый бот читает дифф. Он не запускает
cargo clippy --all-targetsс нашим точным конфигом линтера и не гоняет интеграционный набор за нужными feature-флагами. Ревьюер, который не может выполнить настоящие проверки проекта, сводится к pattern-matching по тексту — а это ровно то место, откуда берутся галлюцинации. - Агент работает на раннере, которому мы уже доверяем. Тот же checkout, та же граница секретов, те же правила сетевого egress. Ничто из диффа не уходит туда, что мы ещё не авторизовали.
- Это конфиг-файл в репозитории, ревьюимый как любой другой. Поведение ревьюера — это workflow-YAML и несколько скриптов в
.agents/tools/под контролем версий. Когда он становится шумным, вы чините это в PR. Когда новичок спрашивает «а что бот вообще проверяет», ответ —git log.
Если вы читали нашу более раннюю заметку о том, почему кастомные MCP должны жить в репозитории, это та же философия, наведённая на CI: инструменты, которые управляют вашим агентом, живут рядом с кодом, над которым они работают.
Как octomind run ведёт себя неинтерактивно
Всё начинается с одного факта об агенте: octomind run переключается в неинтерактивный режим в тот момент, когда вы даёте ему --format или передаёте stdin по пайпу. Из определения CLI:
--format <FORMAT> Output format: plain or jsonl. When set, runs
non-interactively (reads input from stdin).
Внутри рантайм определяет режим вывода по флагу и по тому, является ли stdin терминалом:
| Вызов | Режим |
|---|---|
octomind run в терминале |
Interactive (промпты, цвета, анимации) |
echo "..." | octomind run (по пайпу) |
NonInteractive |
octomind run --format plain |
NonInteractive |
octomind run --format jsonl |
Jsonl (один JSON-объект на строку) |
CI — это случай пайпа, и --format jsonl — то, что вам там нужно. Он подавляет интерактивную мишуру и выдаёт поток структурированных событий — сообщения ассистента, финальное событие стоимости, — который workflow может распарсить, не скребя прозу. В этом и вся причина, почему агент автоматизируем: нет скрытого промпта, ждущего, пока человек нажмёт y.
Можно прогнать это руками, чтобы увидеть:
echo "Review the staged diff and report only real defects." \
| octomind run developer:rust --format jsonl
Последнее событие assistant несёт текст ревью; завершающее событие cost несёт итоги по токенам и долларам. Это тот же поток, который парсит GitHub Action.
Action и его реальные inputs
octomind-action поставляет две точки входа. Мы используем обе для разных задач:
muvon/octomind-action@v1(алиасmuvon/octomind-action/run@v1) запускает одну сессиюoctomind runпо промпту.muvon/octomind-action/workflow@v1запускает многошаговыйoctomind workflowиз TOML-файла.
Для ревью кода достаточно одиночной сессии run. Вот её реальные inputs — скопированы из action.yml, не выдуманы:
| Input | По умолчанию | Что делает |
|---|---|---|
prompt |
(обязателен) | Задача/сообщение, отправляемое octomind |
role |
из конфига | Тег роли/агента, напр. developer:rust |
model |
— | Override модели, напр. openrouter:anthropic/claude-sonnet-5 |
name |
— | Именованная сессия (создать или возобновить) |
resume |
— | Возобновить именованную сессию |
resume_recent |
false |
Возобновить самую недавнюю сессию для cwd |
sandbox |
false |
Ограничить запись в ФС рабочей директорией |
version |
latest |
Версия Octomind для установки |
tap |
— | Добавить tap перед запуском |
config |
— | Путь к конфиг-файлу octomind |
comment |
none |
Режим комментария к PR: full, compact или none |
github_token |
${{ github.token }} |
Токен для публикации комментария к PR |
И outputs, которые можно прочитать в последующих шагах:
| Output | Что несёт |
|---|---|
result |
Последнее сообщение ассистента (само ревью) |
session_id |
ID сессии, для возобновления в следующем шаге |
cost |
{"tokens": N, "cost": N} |
raw_output |
Полный поток JSONL |
exit_code |
Код возврата процесса |
Два из них несущие для ревьюера. comment: compact публикует ревью свёрнутым блоком <details> в PR — один комментарий, сворачиваемый, со стоимостью в подвале <sub> — вместо стены текста. А cost позволяет класть цену каждого ревью в логи, что важнее, чем звучит.
Подключение реальных lint и test команды через .agents/tools/
Вот та часть, что отделила «реально выдержало» от «выключено за неделю». Ревьюер, который только читает текст, выдумывает проблемы, потому что текст — это всё, что у него есть. Дайте ему возможность запускать ваши настоящие проверки — и он перестанет угадывать.
Octomind находит локальные инструменты проекта в <workdir>/.agents/tools/<name> — скрипты с shebang и заголовком-комментарием, который становится схемой инструмента. Они появляются у модели как local-сервер MCP, без отдельного серверного процесса, без регистрации. Механику мы разбирали в кастомные MCP должны жить в репозитории; вот значимый для CI момент: те же скрипты, что агенты ваших разработчиков используют локально, — это те, что агент-ревьюер запускает в CI, потому что они закоммичены в репозиторий.
Поэтому мы спрятали точные команды команды за именованными инструментами.
.agents/tools/lint:
#!/usr/bin/env bash
# @description Run the project linter exactly as CI does. Returns warnings and errors.
set -euo pipefail
cd "$OCTOMIND_WORKDIR"
cargo clippy --all-targets --all-features -- -D warnings
.agents/tools/test:
#!/usr/bin/env bash
# @description Run the test suite. Defaults to the fast unit set.
# @param scope string One of: unit, all (default: unit)
set -euo pipefail
cd "$OCTOMIND_WORKDIR"
scope="${OCTOMIND_PARAM_SCOPE:-unit}"
case "$scope" in
unit) cargo test --lib ;;
all) cargo test --all-targets ;;
*) echo "Unknown scope: $scope" >&2; exit 2 ;;
esac
Теперь промпт ревьюера может сказать «запусти lint и test и сообщай только о дефектах, которые можешь подтвердить». Агент вызывает именованный инструмент, получает реальный код возврата и реальный вывод и заземляет своё ревью на том, что проект делает на самом деле. Утверждение вроде «это завалит clippy» больше не догадка — агент запустил clippy. Если clippy прошёл, утверждение вообще не пишется.
Это самый большой рычаг против режима отказа «галлюцинированная проблема». Модель плохо предсказывает, компилируется ли ваш код. Она прекрасно справляется с чтением вывода команды, которая его скомпилировала.
Ограничение того, что ревьюеру позволено трогать
Ревьюер должен читать, запускать проверки и докладывать. Он не должен редактировать файлы, пушить или открывать PR. Роли Octomind управляют этим через server_refs (какие серверы инструментов видит роль) и allowed_tools (какие инструменты внутри них). Форма — из поставляемых шаблонов ролей:
[[roles]]
name = "reviewer:strict"
system = """
You are a code reviewer. Run the project's lint and test tools to ground
every claim. Report only defects you can substantiate with tool output or a
specific line. If you find nothing, say so in one sentence. Never edit files.
"""
temperature = 0.1
[roles.mcp]
server_refs = ["filesystem"]
allowed_tools = ["view", "lint", "test"]
Несколько намеренных решений тут. temperature = 0.1, потому что ревьюер должен быть скучным и повторяемым, а не творческим. allowed_tools перечисляет только инструменты чтения и проверки — без записи через shell, без редактирования, — так что даже если модель решит что-то «починить», у неё нет для этого инструмента. А последние две фразы system-промпта — это контроль шума: сообщай только то, что можешь подтвердить; если ничего не нашёл, скажи это одной фразой. Одна эта инструкция превратила наш средний комментарий с четырнадцати «проблем» в, на чистом PR, «Дефектов не найдено».
Для дополнительного «ремня и подтяжек» input sandbox: true у Action ограничивает любую запись в ФС рабочей директорией — так что инструмент, которому действительно нужно записать временный файл, может, но ничто не выходит за пределы checkout.
Разрешения и безопасность в неинтерактивном прогоне
Что людей беспокоит в агенте в CI — это аппрувы. В интерактивной сессии вас спросили бы перед запуском инструмента. В CI спрашивать некого, и в этом суть: в неинтерактивном режиме нет ворот y/n, которые подвесят джоб. Безопасность идёт не от человека в петле — она идёт от того, что петля узкая:
allowed_toolsроли — это белый список для инструментов MCP-серверов. Ревьюер не может вызвать серверный инструмент, которого нет в списке, — это статическая граница, а не суждение в рантайме, от которого модель можно отговорить. Исключение — локальные скрипты.agents/tools/: они доступны всегда, их граница — то, что вы коммитите в этот каталог, а ревью PR видит каждое его изменение.sandbox: trueдержит запись внутри checkout.- Секреты остаются в окружении раннера, никогда в репозитории. Скрипты
.agents/tools/читаютACME_TOKENи компанию из окружения; скрипт закоммичен, секрет — это secret GitHub Actions. Если секрет отсутствует, инструмент завершается с ненулевым кодом и ясным сообщением, а не выдумывает результат. github_tokenограничен комментированием. Дефолтный${{ github.token }}публикует комментарий к PR; модели он как инструмент не отдаётся.
Радиус поражения ревьюера: прочитать файлы, запустить две команды из белого списка, написать один комментарий к PR. Это граница, о которой можно рассуждать — что уже больше, чем можно сказать про «модель пообещала ничего не трогать».
Workflow, который выдержал
Вот реальный джоб ревью, целиком заземлённый на inputs выше:
name: AI Code Review
on:
pull_request:
types: [opened, synchronize]
permissions:
contents: read
pull-requests: write # needed to post the review comment
jobs:
review:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # so the agent can diff against the base
- uses: muvon/octomind-action@v1
with:
role: reviewer:strict
model: openrouter:anthropic/claude-sonnet-5
sandbox: true
comment: compact
prompt: |
Review the changes on this branch against the base.
Run the `lint` and `test` tools to ground your findings.
Report ONLY defects you can substantiate with tool output or a
specific changed line. Group by severity. If nothing is wrong,
reply in one sentence. Do not restate the diff.
env:
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
Заметки по решениям:
role: reviewer:strictнесёт низкую температуру, белый список инструментов и system-промпт «подтверди или молчи». Input промпта — это задача; роль — это характер и ограничители. Держите их раздельно, чтобы настраивать раздельно.model:закреплён, а не пущен на самотёк. Закрепление модели — половина вашего контроля стоимости и почти вся воспроизводимость. Мы закрепляем одну модель класса Sonnet и меняем её только намеренно.comment: compactдаёт один сворачиваемый комментарий на прогон. Наsynchronize(каждый пуш в PR) вы получаете свежее ревью, не превращая тред в бесконечный скролл.OPENROUTER_API_KEY— единственный секрет, нужный джобу. Octomind говорит с OpenRouter, Anthropic, OpenAI, DeepSeek, Google Vertex, AWS Bedrock и Cloudflare; заданная переменная окружения выбирает провайдера.
Это весь ревьюер. Один джоб, один Action, два закоммиченных скрипта-инструмента, одно определение роли.
Как держать стоимость в рамках
Агент, который может запускать инструменты, в принципе может зациклиться навсегда и выставлять счёт навсегда. На практике мы ограничиваем его несколькими скучными, но действенными рычагами:
- Закрепите модель. Закреплённая модель класса Sonnet имеет предсказуемую стоимость за токен. «Самое свежее и лучшее» — это как получить счёт-сюрприз.
- Триггерьте на нужных событиях. Только
openedиsynchronize. Не на каждый комментарий, не на черновые PR (фильтруйте черезif: github.event.pull_request.draft == false, когда нужно). - Читайте output
costи логируйте его. Каждый прогон выдаётcostкак{"tokens": N, "cost": N}. Слейте его в summary джоба. Как только цена каждого ревью лежит в ваших логах Actions, стоимость перестаёт быть ощущением и становится числом, на которое можно повесить порог.
- run: echo "Review cost: ${{ steps.review.outputs.cost }}" >> "$GITHUB_STEP_SUMMARY"
Главный драйвер стоимости — инструменты, а не промпт. Ревьюер, который гоняет ваш полный интеграционный набор на каждый пуш, дорог потому, что набор дорог, а не потому, что агент. Наш инструмент test по умолчанию использует быстрый unit-набор именно по этой причине; агент тянется к scope: all только когда дифф трогает что-то, что этого требует, и даже тогда мы можем ограничить это на уровне роли.
Подводные камни, честно
Недетерминизм. Два прогона на одном диффе могут сформулировать ревью по-разному, и иногда один находит то, чего не находит другой. temperature = 0.1 сильно это уменьшает, но не убивает. Лечение — не гнаться за детерминизмом, а относиться к AI-ревью как к одной совещательной проверке среди нескольких. Оно никогда не блокирует merge само по себе. Это делают человек и настоящие проверки CI.
Шум — это проблема промпта, а не модели. Наш первый ревьюер был громким, потому что мы просили «отревью код», что энтузиастичная модель читает как «найди четырнадцать вещей». Лекарством стали две фразы в system-промпте: подтверждай каждое утверждение; если ничего не нашёл, скажи это одной фразой. Плюс инструменты — половина шума была угадыванием моделью исходов, которые она могла просто запустить. Если ваш ревьюер шумный, чините промпт и дайте ему возможность проверять, прежде чем винить модель.
Секреты в выводе инструментов. Инструмент, который вываливает окружение или подробные логи, может слить секрет прямо в комментарий к PR. Держите вывод инструментов плотным: возвращайте результат lint/test, а не весь мир. Мы узнали это, когда многословный раннер тестов сделал echo строки подключения в тело ревью. Теперь инструменты печатают результаты, а не окружение.
Дифф против base-ref. Используйте fetch-depth: 0 на checkout, иначе агент не сможет сравнить с базовой веткой и в итоге «отревьюит» весь файл целиком. Дешёвая ошибка, раздражающий симптом.
Это второй ревьюер, а не тот самый ревьюер. День, когда вы начнёте верить, что он поймает всё, — это день, когда он пропустит тот самый важный. Это неутомимый первый проход. Уставшие люди всё ещё владеют merge.
Частые вопросы
Нужен ли агенту доступ на запись в мой репозиторий?
Нет. Роль ревьюера перечисляет в allowed_tools только инструменты чтения и проверки, sandbox: true запирает любую запись в checkout, а github_token используется только для публикации комментария. Модели никогда не отдаётся инструмент пуша или редактирования.
Можно ли запускать реальные lint/test команды вместо обобщённых?
В этом вся суть. Положите ваши точные команды в .agents/tools/lint и .agents/tools/test, закоммитьте — и агент запустит их в CI. Те же скрипты, что используют локальные агенты ваших разработчиков. См. кастомные MCP должны жить в репозитории.
Как прочитать стоимость ревью?
Action отдаёт output cost ({"tokens": N, "cost": N}) и полный JSONL через raw_output. Сделайте echo cost в $GITHUB_STEP_SUMMARY — и он в логе каждого прогона.
А если я хочу многошаговый пайплайн, а не одно ревью?
Используйте muvon/octomind-action/workflow@v1 с TOML-файлом: соберите контекст одним шагом, отревьюйте следующим, резюмируйте третьим. То же окружение провайдера, тот же plumbing комментария к PR, плюс input dry_run для валидации пайплайна без траты токенов.
Какие провайдеры моделей работают?
Тот, за которого вы уже платите: OpenRouter, Anthropic, OpenAI, DeepSeek, Google Vertex, AWS Bedrock, Cloudflare. Задайте соответствующий API key в env и закрепите модель через input model.
Сдвиг
Инстинкт с AI-ревьюером — сделать его умнее: лучше модель, длиннее промпт, изощрённее. Что реально выдержало у нас — это сделать его уже и заземлённее. Меньше инструментов, все наши. Низкая температура. System-промпт, вся работа которого — держать модель молчащей, пока она не сможет доказать свою правоту. И возможность запускать настоящие проверки, чтобы она рассуждала от вывода cargo clippy, а не от догадки о том, что cargo clippy мог бы сказать.
Результат — не гениальный ревьюер. Это надёжный: он никогда не устаёт, запускает одни и те же проверки каждый раз и — та часть, что далась дольше всего, — перестал галлюцинировать проблемы из воздуха. Про более широкий набор того, что вышло вокруг этого, остальное есть в обзоре за июнь 2026.
Подключите его сперва на низкорисковом репозитории. Посмотрите несколько ревью. Подкрутите промпт дважды. Потом дайте ему тихо сидеть на ваших PR вторым набором глаз, который никогда не моргает.
— Don
Octomind и octomind-action — открытый код под Apache-2.0. Если ваш ревьюер шумный, лекарство почти всегда — промпт и инструменты; а если нет, откройте issue с JSONL, и мы посмотрим.



