← Блог

undercast: оверлей для OBS, сделанный Fable 5

Сережа Рис · 13 June 2026

claude-codeopen-sourceвайбкодингсубагенты

Рабочий код становится open source продуктом, когда ты проходишь короткий чеклист открытия: убираешь из него всё личное (включая git-историю, не только текст), даёшь имя, вешаешь лицензию, пишешь документацию для человека и для агента. Я раздал эти пункты субагентам, и они прошли их параллельно за вечер. Но ни один пункт нельзя пропустить — особенно историю коммитов: текст чистится за минуту, а историю надо принять осознанно.

Код работает — но это ещё не продукт

У меня был внутренний оверлей для OBS. Бегущая строка внизу картинки, план стрима с галочками по этапам, полноэкранные заставки между темами, виджет для промптов. Zero dependencies, один Node-сервер, состояние строки в одном файле, текст в OBS обновляется мгновенно через SSE. Я просил агентов написать его раньше, под себя, и он делал ровно то, что мне было нужно на стримах.

И вот тут ловушка. Рабочий код, который решает мою задачу, — это начало, а не конец. Открыть его миру — отдельная работа, которая к самому коду почти не относится. В коде нет ничего открытого по умолчанию: в нём зашит мой канал, мой бренд в футере, подписи на русском, операционные заметки в гайде. Всё это надо снять, прежде чем чужой человек сможет просто склонировать репозиторий и запустить.

Я сел и расписал, что именно отделяет «работает у меня» от «можно отдать».

Чеклист открытия

   working code (for me)              open source product
   ┌──────────────────┐              ┌──────────────────┐
   │ obs-overlay       │              │ undercast 0.1.0   │
   │ zero deps         │──┐      ┌───▶│ MIT · en/ru       │
   │ Node + SSE        │  │      │    │ public · v0.1.0   │
   └──────────────────┘  │      │    └──────────────────┘
                         ▼      │
             ┌───────────────────────────┐
             │   OPENING CHECKLIST        │
             │   (one evening, by agents) │
             ├───────────────────────────┤
             │ 1. name      ~20 → npm 404 │
             │ 2. de-person grep == empty │
             │ 3. license   MIT           │
             │ 4. docs      README+CLAUDE │
             │ 5. history   squash ⚠ dangling
             └───────────────────────────┘

Имя

Сначала имя. Я попросил агента проверить кандидатов на свободность — около двадцати вариантов разом, в npm и на GitHub. Логика простая: реестр npm на свободное имя отдаёт 404, занятое — отдаёт страницу пакета. Агент прогнал список, отсёк занятые и принёс мне те, что свободны в обоих местах.

Выбрали undercast. Under + cast — слой ПОД эфиром: строка живёт внизу картинки, под основным изображением. И есть второй смысл — в авиации undercast это сплошной слой облаков под крылом самолёта. Имя легло точно в продукт.

Деперсонализация

Самый объёмный пункт. В коде сидело четыре вида личного: хардкод названия моего канала, бренд в футере заставок, подписи только на русском, операционные заметки в гайде для агента. Каждый агент развязал по-своему. Название канала ушло в переменную окружения с дефолтом. Бренд в футере стал опциональным параметром запроса — хочешь, передаёшь, не хочешь, его нет. Подписи локализовал на два языка, английский по умолчанию. README и гайд для агента (CLAUDE.md) переписал по-английски. А мою личную операционку вынес в отдельный файл под gitignore — чтобы она вообще не попала в публичный репозиторий.

Критерий готовности я задал железный: агент прогоняет grep по списку личных маркеров — имя пользователя, хэндлы, домены, название канала — и показывает, что он пустой. Пусто — значит чисто. Заодно я попросил вычистить инфраструктурные детали из публичных issues перед тем, как флипнуть репозиторий в public — у меня агент ведёт GitHub issues через хуки, так что в них накопилось внутреннего.

Перед открытием отправил агента вычистить личное:

Этот код написан для меня лично и уходит в open source. Найди все личные маркеры в том, что попадёт в git: моё имя пользователя, хэндлы каналов, личные домены, хардкод названия моего канала. По каждому реши: вынести в переменную окружения с дефолтом, сделать опциональным параметром или удалить. Хардкод названия канала замени на чтение из переменной окружения. В конце прогони grep по списку личных маркеров и покажи, что он пустой — это критерий готовности.

Это не моя личная придирка. В чеклисте opensource.guide пункт номер три дословно требует: в истории коммитов, issues и pull request’ах нет секретов и приватных данных. Это требование к публикации, а не сноска внизу страницы.

Единый интерфейс

Раньше всё жило в пяти отдельных скриптах — отдельный для строки, отдельный для плана, отдельный для заставок и так далее. Для меня нормально, я помнил, какой за что отвечает. Для чужого человека это пять вещей, которые надо запомнить. Агент свёл их в одну команду undercast <что-то>, а старые скрипты оставил тонкими обёртками — чтобы ничего не сломалось у того, кто уже привык к старому вызову.

Лицензия

Без лицензии «открытый» код юридически закрыт — по умолчанию все права у автора. Я выбрал MIT. Сто семьдесят одно слово, одно-единственное условие — сохранить notice об авторстве. Нулевое трение для тех, кто захочет взять код к себе. Сравнивать варианты помог choosealicense.com: для маленькой утилиты, которую хочешь раздать без условий, MIT — стандартный ответ.

История — самый коварный пункт

Текст почистили, имя дали, лицензию повесили. Остался последний пункт, и он оказался хитрее всех. Git-история помнит всё, что текстовый grep уже не видит: старые коммиты с прежним названием канала, с моим брендом, со всем, что мы только что вынесли.

Я попросил агента сделать fresh start: схлопнуть всю историю в один первый коммит, переименовать репозиторий, открыть его публично и выпустить релиз v0.1.0. Звучит чисто. Но с историей так просто не выходит.

Схлопывание через git оставляет «висячие» (dangling) коммиты. По данным trufflesecurity squash и force-push НЕ удаляют старые коммиты физически — они остаются доступны по своему хешу, и GitHub хранит их. То есть человек, знающий хеш старого коммита, всё ещё может его вытащить. Единственный гарантированно чистый путь — снести папку .git целиком и инициализировать репозиторий заново, с нуля. Тогда старой истории просто не существует.

В моём случае всё сошлось удачно: репозиторий был приватным всю свою историю, схлопывание прошло ДО открытия в public, поэтому старые хеши не попали ни в публичный GitHub Archive, ни в чьи-то форки — их нет. Практический риск близок к нулю. Но если бы репозиторий хоть раз был публичным со старой историей — только полный fresh start, никаких компромиссов.

Почему я на этом настаиваю. В отчёте за 2026 год GitGuardian насчитал, что за 2025-й в публичные репозитории утекло 28,65 млн хардкод-секретов — на 34% больше, чем годом раньше. И цифра прямо про нашу аудиторию: коммиты с пометкой Claude Code утекают секреты вдвое чаще обычных — 3,2% против 1,5% по всему GitHub. Когда код пишет агент, а ты не вычитываешь каждую строку, шанс зашить лишнее в историю растёт, не падает.

Ещё одна вещь, которую я сделал заодно: оставил в корне публичного репозитория гайд для агентов. CLAUDE.md / AGENTS.md в корне — это отдельная практика 2026 года, формат AGENTS.md уже приняли больше 60 000 репозиториев. Мой CLAUDE.md из внутренней шпаргалки превратился в публичный гайд для будущих агентов-контрибьюторов: чужой человек натравливает на репозиторий своего Claude Code, и тот сразу знает, как тут всё устроено.

Раздавал я эти пункты не в один поток. Всем дирижировал оркестратор на Claude Fable 5: он раздавал задачи субагентам, и те шли параллельно — каждый по своему куску, по общему контракту. Тут пригодилось понимание, как выбирать модель для параллельных субагентов: код пишет Sonnet, архитектуру и ревью держит Opus, статусные проверки уходят на Haiku.

Раздал четырём субагентам на Sonnet 4.6, всем один контекст:

Мы пишем оверлей для OBS в четыре потока параллельно, файлы не пересекаются: сервер состояния, рендер строки на клиенте, демон чтения YouTube live-чата, авто-план из транскрипта. Зафиксируй и не меняй контракт состояния: у объекта есть поля mode (план / сообщение / выключено), queue (очередь сообщений на печать) и message (текущее). Клиент печатает текст посимвольно со скоростью 35 миллисекунд на символ — это число одинаково на сервере и на клиенте, не выбирай своё. Реализуй только свою половину по этому контракту. Если поле контракта неудобно — не меняй молча, сначала скажи мне.

Готовый код четырёх половин я отдал на ревью отдельному агенту — уже на Opus, потому что ревью и поиск расхождений между частями требуют другой глубины, чем написание.

Готовый код отправил на ревью отдельному агенту на Opus 4.8:

Прочитай весь код четырёх половин как единое целое. Найди расхождения между серверной и клиентской частью, необработанные сетевые ошибки, гонки в очереди печати, краевые случаи live-чата. Раздели находки на блокеры (нельзя в эфир) и улучшения. По каждой — файл, проблема, как чинить.

Опус вернул находки, разделённые на блокеры и улучшения. Блокеров — ноль. Можно в эфир.

Баг в эфире — доказательство, что продукт живой

Вся эта продуктизация шла поверх кода, который в тот же день крутился на настоящем стриме. Не на демо-фоне, не в песочнице — на живом эфире, со зрителями в чате.

И там вылез баг. Чат не доходил до строки. Совсем. Демон, который читает YouTube live-чат, дёргал API по имени ресурса liveChatMessages — а правильный REST-путь к нему liveChat/messages. Имя ресурса одно, путь к нему другой. Из-за этого был вечный 404, и он маскировался под безобидное «эфир ещё не начался» — то есть выглядел не как ошибка, а как нормальное состояние пустого чата.

Я попросил агента разобраться, почему молчит чат. Агент не стал гадать — дёрнул API напрямую и увидел, что в чате уже 68 сообщений. Значит, проблема не в пустоте, а в том, как мы стучимся. Это та самая архитектура data layer для работы с YouTube API, где слой доступа к данным должен сам показывать, что реально приходит, а не верить коду на слово. Фикс — одна строка: поправить путь. Всё это, не останавливая эфир. После правки 8 сообщений зрителей дошли до строки и поехали по экрану.

И дальше случилось то, ради чего вообще всё это делается. Один зритель написал в чат: «можно сверху вывести такую же полосу, где будут со…» — и через полминуты сам же: «а блин, уже есть такая))». Фича подтвердилась голосом аудитории, без всякого опроса. Параллельно companion-демон, который слушает транскрипт стрима, сам построил пять этапов плана прямо из живого разговора и двигал галочку по ходу эфира.

Вот это для меня и есть «продукт живой». Не тесты зелёные — а зритель, который захотел фичу и тут же её нашёл.

Что я понял

Рабочий код и open source продукт — разные вещи, и расстояние между ними меряется не строчками, а чеклистом открытия. Назвать, деперсонализировать, лицензировать, задокументировать, разобраться с историей — пять пунктов, и ни один не из тех, что можно тихо пропустить.

Но и пугаться объёма не стоит. Через агентов этот чеклист стоит вечер, а не недели. Четыре субагента чистят, переименовывают и документируют параллельно, пока ты решаешь стратегические вопросы вроде имени и лицензии — те, что машине отдавать не хочется.

Единственный пункт, на котором я бы остановил любого, кто открывает свой код: история. Текст чистится за минуту командой агента. А git-историю надо принять головой — понять, что squash не стирает старое, и решить, нужен ли полный fresh start. У меня репозиторий всю жизнь был приватным, и это спасло. Если твой хоть раз светился публично со старой историей — сноси .git и начинай заново, без вариантов.

Имя undercast зарезервировано, репозиторий открыт, релиз v0.1.0 на месте. Опубликовать пакет в реестр — следующий шаг.

Подписаться на обновления — @sereja_tech