Сорок раундов ревью на код, который уже был отревьюен: что два ИИ-ревьюера нашли друг за другом
В прошлой статье про skillmem — локальную память для кодовых агентов — я рассказывал, как чужой README мог стать правилом пользователя, и как мы закрыли это в 0.10. Тогда код прошёл два независимых ревью, и я считал, что дыра одна и она закрыта.
Вчера вышел 0.11.0. Между ними — сорок раундов ревью двумя моделями с разных сторон (одна на стороне Claude, другая на стороне GPT), каждая читала код враждебно и обязана была воспроизвести находку командой, а не рассуждением. Ниже — что из этого вышло и почему я теперь не верю ни одному «чистому» ревью, пока за ним не стоит ещё одно.
Правило стопа
«Ревьюить, пока ошибок не будет» — не терминируется: свежие глаза на любой модуль всегда что-нибудь находят. Мы договорились так: стоп — когда два раунда подряд ни один из двух ревьюеров не воспроизвёл ни одной P1/P2 (потеря данных, неверный ответ, нарушение границы доверия, падение на реальном вводе). P3 копятся в issue следующего релиза.
Правило сработало на раундах 39–40. Дошли мы до них не по прямой.
Три волны находок
Волна 1 (раунды 1–18): исходный аудит. Шесть P1, все не в Stop-хуке, на который все смотрели. HTTP-/write перехватывал чужую запись, /learn писал public-скиллы без права, файлы тел были общими для разных баз, trust можно было выдать любым процессом, kind был path-traversal’ом в экспорте. Закрыли, три раунда чинили то, что сломали фиксы.
Волна 2 (раунды 19–23): репетиция релиза. Я написал ТЗ на релиз и дал его тем же ревьюерам. Репетиция init на копии конфига нашла, что переезд venv удваивает все хуки — рекап шёл бы дважды за сессию. Дальше цепочкой: бэкапы конфигов, названные по секунде, перетирали друг друга; создавались с правами 0644 рядом с файлом, где лежит OAuth-аккаунт; Codex-бэкап был не байт-в-байт. Одиннадцать P2 в коде установки, который «работал».
Волна 3 (раунды 24–40): свежие глаза на ядро. Самое интересное. Каждый раз, когда ревьюер получал модуль, который никто не перечитывал заново, он находил 1–3 старые P2, унаследованные из main:
HTTP-сервер с несколькими агентами: 409 на конфликт цитировал заголовки чужих приватных записей. Бэклинки публичной записи выдавали слаг чужой приватной.
/search,/list,/recallрезали страницу до фильтра видимости — 110 чужих записей, и своя собственная возвращалась пустым 200, хотя/getеё находил.import-vaultшёл по симлинку*.mdнаружу и клал в базу текст цели —~/.zshrc, например. Вложения и паки этот случай уже отвергали; заметки — нет.skills rm <pack>удалял все записи проекта пака, включая авторскую заметку владельца.Windows-планировщик не нёс переменные окружения, которые launchd/cron/systemd несли.
И моё любимое:
scrub— функция, которая прячет секреты перед записью — не была идемпотентной. Уже подставленный[secret redacted]матчился заново при каждой перезаписи и рос в[secret redacted] redacted]. Хеш менялся, одобрение владельца снималось. Restore дампа поверх той же базы снимал одобрения молча.
Каждый второй фикс рождал регрессию
Это главное, что я вынес. Фикс вытеснения в поиске прошёл четыре итерации, и каждую ловил следующий раунд:
Окно кандидатов 5 → фильтр после лимита: пять скрытых строк вытесняют свою.
Окно 100: сто скрытых строк — то же самое.
Без лимита, курсор до N видимых: без
LIMITSQLite сортирует все совпадения до первой строки, и широкий SELECT тащил все тела через сортировку — 20 секунд на запрос на базе в 9 тысяч строк.Узкий проход + тела только для отобранных: 52 мс без фильтра, 73–87 с фильтром. Готово? Нет: предикат отдали и мастеру, и мастер по HTTP стал ранжировать на неограниченном пуле — другой top-5, чем в CLI, для пяти запросов из шести.
Пятая итерация — мастер идёт нефильтрованным путём, и тест «HTTP-мастер == S.search», который падает на предыдущем коммите. Только тогда раунд стал чистым.
Если бы после первого фикса ревью не было — мы бы отгрузили 20-секундный /search с уверенностью, что закрыли утечку.
Что стоит повторить у себя
Два ревьюера с разных сторон, не один. Модели ошибаются по-разному; пересечение их находок за 40 раундов было заметно меньше объединения.
Формат отчёта жёсткий:
P1|P2|P3 · файл:строка · что · evidence (команда + вывод). «Кажется, тут гонка» — это P3, не гейт.Ревьюер не пишет код. Дважды за цикл ревьюер нарушал read-only (правка
pyproject.toml, файл рядом с рабочим клоном) — проверяйтеgit statusпосле каждого раунда.Каждый фикс — новый раунд, даже однострочный. Особенно однострочный.
Стоп-правило назвать заранее, иначе цикл либо не кончится, либо кончится там, где устали.
Цена: сутки машинного времени и ~350 тестов вместо 221. Итог 0.11.0: граница доверия закрыта на HTTP, MCP, CLI и хуках одинаково, и каждая строка release notes проверена командой до того, как была написана. Что осталось P3 — в issue 0.11.1.
pip install skillmem · GitHub · Release notes 0.11.0
Английская версия — на dev.to: https://dev.to/sergey_petrukovich_c94a17/forty-review-rounds-on-code-that-had-already-been-reviewed-46mn
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.