skillmem — это локальная память для кодовых агентов: инструмент хранит заметки, скиллы и контекст проекта на машине разработчика и отдаёт их агенту через HTTP, MCP, CLI и хуки. Версия 0.10.0 прошла два независимых ревью, и автор считал, что единственная дыра — подмена правила пользователя через чужой README — закрыта. Версия 0.11.0 вышла после сорока раундов ревью двумя моделями: одна на стороне Claude, другая на стороне GPT. Каждая читала код враждебно и обязана была воспроизвести находку командой, а не рассуждением.

Правило стопа сформулировали заранее: цикл останавливается, когда два раунда подряд ни один из двух ревьюеров не воспроизвёл ни одной P1 или P2 — то есть потери данных, неверного ответа, нарушения границы доверия или падения на реальном вводе. Замечания уровня P3 копятся в issue следующего релиза. Правило сработало на раундах 39–40. До них дошли не по прямой.

ВолнаРаундыЧто нашли
Исходный аудит1–18Шесть P1: перехват чужой записи через HTTP, /learn без права, общие файлы тел, выдача доверия любым процессом, path-traversal в kind
Репетиция релиза19–23Одиннадцать P2 в установщике: дублирование хуков при переезде venv, перетирание бэкапов, права 0644 рядом с OAuth-файлом, не байт-в-байт бэкап Codex
Свежие глаза на ядро24–40Утечки приватных записей через 409, бэклинки и поиск; обход по симлинку *.md; удаление авторской заметки; неидемпотентный scrub

Первая волна, раунды 1–18, — исходный аудит. Шесть P1, и ни одна не в Stop-хуке, на который все смотрели. HTTP-перехватчик записи пропускал чужую запись. Эндпоинт /learn писал публичные скиллы без права на это. Файлы тел оказались общими для разных баз. Доверие можно было выдать любым процессом. Поле kind работало как path-traversal в экспорте. После закрытия три раунда ушли на починку того, что сломали сами фиксы.

Волна репетиции релиза вскрыла одиннадцать P2 в установщике: дублирование хуков при переезде venv, перетирание бэкапов и права 0644 рядом с OAuth-файлом.

Вторая волна, раунды 19–23, — репетиция релиза. Автор написал техническое задание на релиз и отдал его тем же ревьюерам. Прогон init на копии конфига показал, что переезд venv удваивает все хуки: рекап шёл бы дважды за сессию. Дальше цепочкой: бэкапы конфигов, названные по секунде, перетирали друг друга; создавались с правами 0644 рядом с файлом, где лежит OAuth-аккаунт; бэкап Codex был не байт-в-байт. Одиннадцать P2 в коде установки, который до этого «работал».

Третья волна, раунды 24–40, — свежие глаза на ядро. Каждый раз, когда ревьюер получал модуль, который никто не перечитывал заново, он находил одну-три старые P2, унаследованные из main. HTTP-сервер с несколькими агентами: 409 на конфликт цитировал заголовки чужих приватных записей. Бэклинки публичной записи выдавали слаг чужой приватной. Эндпоинты /search, /list и /recall резали страницу до фильтра видимости — возвращали 110 чужих записей, а собственная запись приходила пустым 200, хотя /get её находил. Импорт vault шёл по симлинку *.md наружу и клал в базу текст цели, например ~/.zshrc. Вложения и паки этот случай уже отвергали, заметки — нет. Команда skills rm <pack> удаляла все записи проекта пака, включая авторскую заметку владельца. Windows-планировщик не нёс переменные окружения, которые launchd, cron и systemd несли. И отдельно: функция scrub, которая прячет секреты перед записью, не была идемпотентной — уже подставленный маркер [secret redacted] матчился заново при каждой перезаписи и рос в [secret redacted] redacted]. Хеш менялся, одобрение владельца снималось. Restore дампа поверх той же базы снимал одобрения молча.

Главный вывод автора: каждый второй фикс рождал регрессию. Фикс вытеснения в поиске прошёл четыре итерации, и каждую ловил следующий раунд. Окно кандидатов из пяти строк с фильтром после лимита: пять скрытых строк вытесняют свою. Окно в сто строк: сто скрытых строк дают тот же эффект. Убрали лимит, поставили курсор до N видимых: без LIMIT SQLite сортирует все совпадения до первой строки, и широкий SELECT тащил все тела через сортировку — 20 секунд на запрос на базе в девять тысяч строк. Узкий проход с телами только для отобранных: 52 мс без фильтра, 73–87 мс с фильтром. И снова нет: предикат отдали и мастеру, а мастер по HTTP стал ранжировать на неограниченном пуле — другой top-5, чем в CLI, для пяти запросов из шести. Пятая итерация — мастер идёт нефильтрованным путём, и тест «HTTP-мастер == S.search», который падает на предыдущем коммите. Только тогда раунд стал чистым. Без повторного ревью после первого фикса отгрузили бы 20-секундный /search с уверенностью, что закрыли утечку.

Что автор советует повторить у себя. Два ревьюера с разных сторон, а не один: модели ошибаются по-разному, и пересечение их находок за сорок раундов было заметно меньше объединения. Формат отчёта жёсткий: 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.