undercast: оверлей для OBS, сделанный Fable 5
Рабочий код становится 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